Взаимоотношения в рельсах 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, но к той же таблице, что-то вроде того, как подробно описано отношением " многие ко многим" с той же моделью в рельсах?, Однако, поскольку вы хотите, чтобы отношение было двунаправленным ("все мои друзья тоже друзья со мной"), у вас есть два варианта:

  1. Используйте одну таблицу соединений, каждая строка которой связывает два 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].
    
  2. Используйте одну запись, но взломайте, чтобы определить, с кем вы дружите:

    # 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 %>
Другие вопросы по тегам