Search code examples
ruby-on-railspolymorphismhas-many-throughpolymorphic-associations

Many to many with polymorphic association doesn't work in both directions


I am implementig the system that enable user to follow the "followable"( in my case these may be an event, place or other user).

My idea:

Follow model holds the user_id, followable type and followale_id(join table)

class Follow < ActiveRecord::Base
  belongs_to :user
  belongs_to :followable, polymorphic: true
end

Event

class Event < ActiveRecord::Base    
  has_many :follows, as: :followable
  has_many :users, through: :follows
end

Place

class Place < ActiveRecord::Base
  has_many :follows, as: :followable
  has_many :users, through: :follows    
end

User

class User < ActiveRecord::Base
  has_many :follows
  has_many :events, through: :follows, source: :followable, source_type: "Event"
  has_many :places, through: :follows, source: :followable, source_type: "Place"
  has_many :users, through: :follows, source: :followable, source_type: "User"
end

The problem is that the realtion works only in one direction, i can do:

user.follows.create(followable:event1) #follow event1
user.follows.create(followable:place1) #follow place1
user.follows.create(followable:user1)  #follow user1
user.follows # displays all following relations user has established

But, i cannot do:

event1.follows #return follow objects(some user - event1 pairs)
event1.users #return all of the users that follow this event
user1.users  #return all of the users that user1 follows, the most confusing part..

All of the aboves return nil.

How should i establish the relations to make it work in both directions?
Also, i'd like to hear some remarks on how to improve this idea, beacouse it's the first time i'm playin around with more complex realtions. Thank you in advance.


Solution

  • Lets start off with the User model as its the trickiest:

    class User < ActiveRecord::Base
      has_many :follows, source: :user
      has_many :follows_as_fallowable, 
                        class_name: 'Follow',
                        as: :followable
    
      has_many :followers, through: :follows_as_fallowable,
                           source: :user
    
      # other users the user is following                     
      has_many :followed_users, through: :follows,
                                source: :followable,
                                source_type: 'User'
    end
    

    Note that we need two different relationships to follows due to the fact that the user can be in either column depending on if the user is the follower or the object being followed.

    We can now do a simple test to check if the relationship is setup properly:

    joe = User.create(name: 'Joe')
    jill = User.create(name: 'Jill')
    joe.followers << jill
    jill.followed_users.include?(joe) # true
    joe.followers.include?(jill) # true
    

    To then setup a the bi-directional relation between users and a followable model you would do:

    class Event < ActiveRecord::Base
      has_many :follows, as: :followable
      has_many :followers, through: :follows,
                           source: :user
    end
    
    class User < ActiveRecord::Base
      # ...
      has_many :followed_events, through: :follows,
                                 source: :followable,
                                 source_type: 'Event'
    end
    

    The relations in the followed model (Event) are pretty much the same in every model so you can easily extract it to a module for reuse:

    # app/models/concerns/followable.rb
    module Followable
      extend ActiveSupport::Concern
    
      included do
        has_many :follows, as: :followable
        has_many :followers, through: :follows,
                             source: :user
      end
    end
    
    class Event < ActiceRecord::Base
      include Followable
    end
    
    class Place < ActiceRecord::Base
      include Followable
    end