Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Many-to-many self join in rails?

Rails documentation provides a nice explanation of how to handle a self join where only a has_many-belongs_to relationship is required. In the example, an employee (as a manager) can have many employees (each, as a subordinate).

However, how do you handle a has_many-has_many self join (which I've heard referred to as a bi-directional looped association)?

For example, how do you handle the situation in which an employee can have many subordinates, in its capacity as manager, and also have many managers, in its capacity as subordinate?

Or, in other words, where a user can follow many users and be followed by many users?

like image 645
jbmilgrom Avatar asked Nov 30 '22 18:11

jbmilgrom


1 Answers

A User can have many:

  • followers in its capacity as followee
  • followees in its capacity as follower.

Here's how the code for user.rb might look:

class User < ActiveRecord::Base
  # follower_follows "names" the Follow join table for accessing through the follower association
  has_many :follower_follows, foreign_key: :followee_id, class_name: "Follow" 
  # source: :follower matches with the belong_to :follower identification in the Follow model 
  has_many :followers, through: :follower_follows, source: :follower

  # followee_follows "names" the Follow join table for accessing through the followee association
  has_many :followee_follows, foreign_key: :follower_id, class_name: "Follow"    
  # source: :followee matches with the belong_to :followee identification in the Follow model   
  has_many :followees, through: :followee_follows, source: :followee
end

Here's how the code for follow.rb:

class Follow < ActiveRecord::Base
  belongs_to :follower, foreign_key: "follower_id", class_name: "User"
  belongs_to :followee, foreign_key: "followee_id", class_name: "User"
end

The most important things to note are probably the terms :follower_follows and :followee_follows in user.rb. To use a run of the mill (non-looped) association as an example, a Team may have many :players through :contracts. This is no different for a Player, who may have many :teams through :contracts as well (over the course of such Player's career).

But in this case, where only one named model exists (i.e. a User), naming the through: relationship identically (e.g. through: :follow) would result in a naming collision for different use cases of (or access points into) the join table. :follower_follows and :followee_follows were created to avoid such a naming collision.

Now, a User can have many :followers through :follower_follows and many :followees through :followee_follows:

  • To determine a User’s :followees (upon an @user.followees call to the database), Rails may now look at each instance of class_name: “Follow” where such User is the the follower (i.e. foreign_key: :follower_id) through: such User’s :followee_follows.
  • To determine a User’s :followers (upon an @user.followers call to the database), Rails may now look at each instance of class_name: “Follow” where such User is the the followee (i.e. foreign_key: :followee_id) through: such User’s :follower_follows.
like image 92
jbmilgrom Avatar answered Dec 06 '22 17:12

jbmilgrom