Взаимоотношения в рельсах 4
Я пытаюсь разрешить пользователям моего приложения дружить друг с другом с помощью запросов на добавление в друзья, и меня немного смущает то, как работают отношения... Когда дружба создается одним пользователем и принимается другим, Мне бы хотелось, чтобы дружба была видна обоим пользователям (очевидно).
Я хотел бы добиться реализации, которая позволит мне сделать что-то похожее на следующее:
user1 friend requests user2
user2 accepts
user1.friends now contains user2
user2.friends now contains user1
Вот что у меня есть, но я прочитал некоторые странные вещи о вложенном has_many: через отношения
class User < ActiveRecord::Base
has_many :friendships
has_many :friends, :class_name => "User", :through => :friendships
end
class Friendship < ActiveRecord::Base
has_many :users, :limit => 2
end
Это жизнеспособная реализация? Если нет, что я могу изменить / улучшить? Я хотел бы избежать 2 строк, представляющих одно отношение, если это возможно.
2 ответа
Что вы ищете, так это отношение has_and_belongs_to_many, но к той же таблице, что-то вроде того, как подробно описано отношением " многие ко многим" с той же моделью в рельсах?, Однако, поскольку вы хотите, чтобы отношение было двунаправленным ("все мои друзья тоже друзья со мной"), у вас есть два варианта:
Используйте одну таблицу соединений, каждая строка которой связывает два user_ids, но вставьте две строки для каждой дружбы.
# no need for extra columns on User class User < ActiveRecord::Base has_many :friendships has_many :friends, through: :friendships end # t.belongs_to :user; t.belongs_to :friend class Friendship < ActiveRecord::Base belongs_to :user belongs_to :friend, class_name: "User" end u1 = User.create! u2 = User.create! u3 = User.create! # make users 1 and 2 friends u1.friendships.create(friend: u2) u2.friendships.create(friend: u1) # make users 2 and 3 friends u2.friendships.create(friend: u3) u3.friendships.create(friend: u2) # and now, u1.friends returns [u1], # u2.friends returns [u1, u3] and # u3.friends returns [u2].
Используйте одну запись, но взломайте, чтобы определить, с кем вы дружите:
# no need for extra columns on User class User < ActiveRecord::Base has_many :friendships_as_a, class_name: "Friendship", foreign_key: :user_a_id has_many :friendships_as_b, class_name: "Friendship", foreign_key: :user_b_id def friends User.where(id: friendships_as_a.pluck(:user_b_id) + friendships_as_b.pluck(:user_a_id)) end end # t.belongs_to :user_a; t.belongs_to :user_b class Friendship < ActiveRecord::Base belongs_to :user_a, class_name: "User" belongs_to :user_b, class_name: "User" end
Это не самый чистый способ сделать это, но я думаю, вы обнаружите, что на самом деле нет такого чистого способа при такой настройке (с денормализованной таблицей). Вариант 1 намного безопаснее. Вы также можете использовать представление SQL, чтобы достичь среднего уровня, автоматически генерируя зеркальные записи для каждой дружбы.
Изменить: Миграция и использование в API
В соответствии с комментарием ОП ниже, чтобы полностью использовать Вариант 1, вот что вам нужно сделать:
rails g migration CreateFriendships
Отредактируйте этот файл так:
class CreateFriendships < ActiveRecord::Migration
create_table :friendships do |t|
t.belongs_to :user
t.belongs_to :friend
t.timestamps
end
end
Создать модель Дружбы:
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, class_name: "User"
end
Тогда на вашей модели пользователя:
class User < ActiveRecord::Base
# ...
has_many :friendships
has_many :friends, through: :friendships, class_name: 'User'
# ...
end
А в вашем API скажем новый FriendshipsController:
class FriendshipsController < ApplicationController
def create
friend = User.find(params[:friend_id])
User.transaction do # ensure both steps happen, or neither happen
Friendship.create!(user: current_user, friend: friend)
Friendship.create!(user: friend, friend: current_user)
end
end
end
Как выглядит ваш маршрут (в config/routes.rb
):
resource :friendships, only: [:create]
И просьба будет выглядеть так:
POST /friendships?friend_id=42
Тогда вы можете обратиться к current_user.friends
всякий раз, когда вы хотите узнать, с кем дружит пользователь.
Вы бы использовали has_many :through
:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :friendships
has_many :friends, through: :friendships, -> { where(status: "accepted") }
end
#app/models/friendship.rb
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, class_name: "User"
enum status: [:pending, :accepted]
validates :user, uniqueness: { scope: :friend, message: "You can only add a friend once" }
def decline
self.destroy
end
def accept
self.update status: "approved"
end
end
Выше это self-referential join
, что позволяет следующее:
@user = User.find params[:id]
@friend = User.find params[:friend_id]
@user.friends << @friend
-
Это добавит новый friendship
для пользователя с его значением по умолчанию status
установлен в pending
, @user.friends
ассоциация установлена так, что только accepted
друзья появляются из звонка.
Таким образом, вы сможете сделать следующее:
#config/routes.rb
resources :users do
resources :friendships, only: [:index, :destroy, :update], path_names: { destroy: "remove", update: "accept" }
end
#app/controllers/Frienships_controller.rb
class FriendshipsController < ApplicationController
def index
@user = User.find params[:user_id]
@friendship = @user.friendships
end
def update
@user = User.find params[:user_id]
@friendship = @user.friendships.find params[:id]
@friendship.accept
end
def destroy
@user = User.find params[:user_id]
@friendship = @user.friendships.find params[:id]
@friendship.decline
end
end
#app/views/friendships/index.html.erb
<%= @friendships.pending.each do |friendship| %>
<%= link_to "Accept", user_friendships_path(user, friendship), method: :put %>
<%= link_to "Decline", user_friendships_path(user, friendship), method: :delete %>
<% end %>