【问题标题】:How to model a mutual friendship in Rails如何在 Rails 中建立相互友谊
【发布时间】:2016-09-11 16:10:51
【问题描述】:

我知道以前在 Stack Overflow 上有人问过这个问题,但答案并没有以我可以解释的方式为我做。我的一般方法受到this 教程的启发。

我正在尝试做的是创建一个非常简单的模型,用于与用户建立好友关系,通过一条记录在两端创建等效的友谊。

在数据库级别,我只有一个“friendships”表,它只有一个 user_id、一个friend_id 和一个 is_pending 布尔列。

在 user.rb 中,我将关系定义为:

has_many :friendships
has_many :friends, through: :friendships

在friendship.rb中,我将关系定义为:

belongs_to :user
belongs_to :friend, :class_name => 'User'

如果我添加好友,我可以如下访问:

> a = User.first
> b = User.last
> Friendship.new(a.id, b.id)
> a.friends
=> #<User b>

这很完美,但我想要的是也能够像这样朝另一个方向发展:

> b.friends

不幸的是,按原样定义关系后,我得到了一个空集合。运行的 SQL 显示它正在搜索 user_id = b.id。如何指定它也应该搜索friend_id = b.id?

【问题讨论】:

  • 你最终决定了什么?好奇的人想知道!
  • @MichaelGaskill 我想我将删除关系并在 user.rb 上定义一个 Friends 方法,该方法将运行一个简单的查询。我计划今晚结束这个功能,如果我没有改变主意,我会粘贴我的最终代码和一篇文章。

标签: ruby-on-rails ruby oop activerecord relationships


【解决方案1】:

也许是这样的:

friendship.rb
belongs_to :friend_one, :foreign_key => :user_id
belongs_to :friend_two, :foreign_key => :friendship_id

user.rb

has_many :friendship_ones, :class_name => 'Friendship', :foreign_key => :friendship_id
has_many :friend_ones, through: :friendship_ones
has_many :friendship_twos, :class_name => 'Friendship', :foreign_key => :user_id
has_many :friend_twos, through: :friendship_twos


def friends
  friend_ones + friend_twos
end

您有两个查询来查找朋友,但它是一个简单的数据模型,您只需调用@user.friends 来查找实例。

如果您加载了两个friend_ones 和friend_twos 关联,则可以进行预加载。

【讨论】:

  • 嗯。到那时,只做 has_many :friendships,然后用一点自定义 ActiveRecord 查询定义朋友不是更有意义吗?
  • 可能是。这是另一种值得写的方法。
  • 不错的整体解决方案。您可以让 friends 方法简单地执行连接查询以获得您想要的结果。
【解决方案2】:

这也可以通过单个 has_many :through 关联和一些查询摆弄来实现:

# app/models/friendship.rb

class Friendship < ApplicationRecord
  belongs_to :user
  belongs_to :friend, class_name: 'User'
end

 

# app/models/user.rb

class User < ApplicationRecord
  has_many :friendships,
    ->(user) { FriendshipsQuery.both_ways(user_id: user.id) },
    inverse_of: :user,
    dependent: :destroy

  has_many :friends,
    ->(user) { UsersQuery.friends(user_id: user.id, scope: true) },
    through: :friendships
end

 

# app/queries/friendships_query.rb

module FriendshipsQuery
  extend self

  def both_ways(user_id:)
    relation.unscope(where: :user_id)
      .where(user_id: user_id)
      .or(relation.where(friend_id: user_id))
  end

  private

  def relation
    @relation ||= Friendship.all
  end
end

 

# app/queries/users_query.rb

module UsersQuery
  extend self

  def friends(user_id:, scope: false)
    query = relation.joins(sql(scope: scope)).where.not(id: user_id)

    query.where(friendships: { user_id: user_id })
      .or(query.where(friendships: { friend_id: user_id }))
  end

  private

  def relation
    @relation ||= User.all
  end

  def sql(scope: false)
    if scope
      <<~SQL
        OR users.id = friendships.user_id
      SQL
    else
      <<~SQL
        INNER JOIN friendships
          ON users.id = friendships.friend_id
          OR users.id = friendships.user_id
      SQL
    end
  end
end

它可能不是最简单的,但它肯定是最干燥的。它不使用任何回调、附加记录和关联,并保持关联方法不变,包括隐式关联创建:

user.friends << new_friend

通过gist.github.com

【讨论】:

  • 只想说,这是个美人!赞一个!
【解决方案3】:

本文展示了如何建立互惠关系:Bi-directional relationships in Rails

它展示了如何使用after_createafter_destroy 插入模拟互惠关系的其他关系。这样一来,您的联接表中的记录就会增加一倍,但您可以灵活地使用 a.friendsb.friends 并看到两者正确地相互包含。

使其适用于您的模型:

class Person < ActiveRecord::Base
  has_many :friendships, :dependent => :destroy
  has_many :friends, :through => :friendships, :source => :person
end

class Friendship < ActiveRecord::Base
  belongs_to :person, :foreign_key => :friend_id
  after_create do |p|
    if !Friendship.find(:first, :conditions => { :friend_id => p.person_id })
      Friendship.create!(:person_id => p.friend_id, :friend_id => p.person_id)
    end
  end
  after_update do |p|
    reciprocal = Friendship.find(:first, :conditions => { :friend_id => p.person_id })
    reciprocal.is_pending = self.is_pending unless reciprocal.nil?
  end
  after_destroy do |p|
    reciprocal = Friendship.find(:first, :conditions => { :friend_id => p.person_id })
    reciprocal.destroy unless reciprocal.nil?
  end
end

我已经在几个项目中成功地使用了这种方法,而且非常方便!

【讨论】:

  • 您有多确定这需要两条记录?我想到了这样做,但我更喜欢用一张唱片来做,并认为会有一种相当简单的方法来做。如果没有,我就把记录翻一番。
  • 当然,您还可以使用 after_update 来维护 is_pending 的值。
猜你喜欢
  • 2015-09-14
  • 2011-12-01
  • 1970-01-01
  • 2015-01-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多