How to model a mutual friendship in Rails

后端 未结 3 520
小蘑菇
小蘑菇 2021-01-03 09:02

I know this question has been asked before on Stack Overflow, but the answers aren\'t doing it for me in ways I can explain. My general approach was inspired by this tutoria

相关标签:
3条回答
  • 2021-01-03 09:32

    This article shows how to set up reciprocal relationships: Bi-directional relationships in Rails

    It shows how to use after_create and after_destroy to insert additional relationships that model the reciprocal relationship. In that way, you'd have double the records in your join table, but you'd have the flexibility of using a.friends and b.friends and seeing that both include each other correctly.

    Making it work with your model:

    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
    

    I've used this approach successfully on a few projects, and the convenience is fantastic!

    0 讨论(0)
  • 2021-01-03 09:47

    Maybe this:

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

    and

    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
    

    You get two queries to find the friends, but it is a simple data model and you you do just call @user.friends to find the instances.

    It would be amenable to eager loading, if you load the two friend_ones and friend_twos associations.

    0 讨论(0)
  • 2021-01-03 09:50

    This is also achievable with a single has_many :through association and some query fiddling:

    # 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
    

    It may not be the simplest of them all but it's certainly the DRYest. It does not use any callbacks, additional records and associations, and keeps the association methods intact, including implicit association creation:

    user.friends << new_friend
    

    via gist.github.com

    0 讨论(0)
提交回复
热议问题