Help with Associations Self Joins Example

I am an experienced software developer learning Rails, I like it a lot by the way.

I was reading through associations and found the Self Joins example so I am trying the example but it is missing some important details I need. Maybe the example assumes I know more than I really do, in my career I have made similar assumptions.

The model looks like this:

class Employee < ApplicationRecord
  has_many :subordinates, class_name: "Employee",
                          foreign_key: "manager_id"

  belongs_to :manager, class_name: "Employee", optional: true
end

The migration schema looks like this:

class CreateEmployees < ActiveRecord::Migration[7.0]
  def change
    create_table :employees do |t|
      t.references :manager, foreign_key: { to_table: :employees }
      t.timestamps
    end
  end
end

The example states I can use the association in the following way:

With this setup, you can retrieve @employee.subordinates and @employee.manager .

This interested me as it looks like I could get a managers subordinates or a subordinate’s manager, pretty exciting. Then reality set in, I don’t know how to associate managers and subordinates in the code, controller I assume. I have searched all over the place to no avail. I realized this piece is missing from the example. Can some one help me with a simple complete example I can sandbox.

Thanks in advance for your help. Remember I need complete details assume I know nothing because for the most part at the stage I am at its true.

The setup you’ve got so far looks pretty well to me – I would recommend adding appropriate inverse_of to the associations in the model:

class Employee < ApplicationRecord
  has_many :subordinates, class_name: "Employee",
                          foreign_key: "manager_id", inverse_of: :manager

  belongs_to :manager, class_name: "Employee", inverse_of: :subordinates, optional: true
end

And perhaps a name column on the employees table, so create an additional migration using bin/rails g migration AddNameToEmployee, inside the generated file add this line:

add_column :employees, :name, :string

and then run bin/rails db:migrate. Then we’re ready to experiment!

The same kind of ActiveRecord code you might write in a controller can be tested out in bin/rails c. Try executing these lines:

burns = Employee.create(name: 'Monty Burns')
grimes = Employee.create(name: 'Frank Grimes', manager: burns)
carl = Employee.create(name: 'Carl Carlson', manager: burns)
lenny = Employee.create(name: 'Lenny Leonard', manager: burns)
homer = Employee.create(name: 'Homer Simpson', manager: grimes)

lenny.manager.name
#=> "Monty Burns"

burns.subordinates.pluck(:name)
#=> ["Frank Grimes", "Carl Carlson", "Lenny Leonard"]

homer.manager.manager.name
#=> "Monty Burns"

lenny.subordinates
#=> []

lenny.subordinates.class
#=> Employee::ActiveRecord_Associations_CollectionProxy

For that last example note that what ActiveRecord brings back from a has_many is a special container object. This is so that other filtering or ordering or other stuff can still be applied on the end of this thing, and have it all to be run as one database query.

Let me know how you get on!

Very nice, I was able to make it work with the console just as you laid it out! I tried it without your Model changes and that worked fine. After reading the explanation, the bi-directional function is not required but there must be some reason you think it would be good to include. Can you please elaborate?

My next step will be to translate the step by step actions into logic, just the way I like to approach development. Hoping I can do that myself, should be able to.

1 Like

The reason to add inverse_of on both sides is so that when ActiveRecord loads related objects, it is able to recognize stuff that’s already in memory and just use that instead of going out and getting another copy of the same thing. Not only does this save a little bit of memory and database access load, it also means you don’t end up with the same object multiple times but with different object_id.

If you have an app where you sometimes have to do some_ar_object.reload in order to get things to work, you might be able to clean it up by making sure the inverse_of things in your model are clean.

Note that if you have associations that do not use class_name and foreign_key then Rails is smart and generally auto-creates appropriate inverse_of behind the scenes for you. It’s only when you get a little fancy with things that it can not automatically determine the inverse_of.

Thanks for taking the time to explain this to me. it’s much appreciated!

I did add the inverse_of key word, and have logic working in my code. You have been a great help with exactly the correct amount of information to get me on a self sufficient tract.

1 Like

Grateful that it’s worked out well. Rails is a very fun ecosystem!