Rails association with multiple foreign keys Rails association with multiple foreign keys ruby-on-rails ruby-on-rails

Rails association with multiple foreign keys


TL;DR

class User < ActiveRecord::Base  def tasks    Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)  endend

Remove has_many :tasks in User class.


Using has_many :tasks doesn't make sense at all as we do not have any column named user_id in table tasks.

What I did to solve the issue in my case is:

class User < ActiveRecord::Base  has_many :owned_tasks,    class_name: "Task", foreign_key: "owner_id"  has_many :assigned_tasks, class_name: "Task", foreign_key: "assignee_id"endclass Task < ActiveRecord::Base  belongs_to :owner,    class_name: "User", foreign_key: "owner_id"  belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"  # Mentioning `foreign_keys` is not necessary in this class, since  # we've already mentioned `belongs_to :owner`, and Rails will anticipate  # foreign_keys automatically. Thanks to @jeffdill2 for mentioning this thing   # in the comment.end

This way, you can call User.first.assigned_tasks as well as User.first.owned_tasks.

Now, you can define a method called tasks that returns the combination of assigned_tasks and owned_tasks.

That could be a good solution as far the readability goes, but from performance point of view, it wouldn't be that much good as now, in order to get the tasks, two queries will be issued instead of once, and then, the result of those two queries need to be joined as well.

So in order to get the tasks that belong to a user, we would define a custom tasks method in User class in the following way:

def tasks  Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)end

This way, it will fetch all the results in one single query, and we wouldn't have to merge or combine any results.


Extending upon @dre-hh's answer above, which I found no longer works as expected in Rails 5. It appears Rails 5 now includes a default where clause to the effect of WHERE tasks.user_id = ?, which fails as there is no user_id column in this scenario.

I've found it is still possible to get it working with a has_many association, you just need to unscope this additional where clause added by Rails.

class User < ApplicationRecord  has_many :tasks, ->(user) {    unscope(:where).where(owner: user).or(where(assignee: user)  }end


Rails 5:

you need to unscope the default where clause see @Dwight answer if you still want a has_many associaiton.

Though User.joins(:tasks) gives me

ArgumentError: The association scope 'tasks' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.

As it is no longer possible you can use @Arslan Ali solution as well.

Rails 4:

class User < ActiveRecord::Base  has_many :tasks, ->(user){ where("tasks.owner_id = :user_id OR tasks.assignee_id = :user_id", user_id: user.id) }end

Update1: Regarding @JonathanSimmons comment

Having to pass the user object into the scope on the User model seems like a backwards approach

You don't have to pass the user model to this scope. The current user instance is passed automatically to this lambda. Call it like this:

user = User.find(9001)user.tasks

Update2:

if possible could you expand this answer to explain what's happening? I'd like to understand it better so I can implement something similar. thanks

Calling has_many :tasks on ActiveRecord class will store a lambda function in some class variable and is just a fancy way to generate a tasks method on its object, which will call this lambda. The generated method would look similar to following pseudocode:

class User  def tasks   #define join query   query = self.class.joins('tasks ON ...')   #execute tasks_lambda on the query instance and pass self to the lambda   query.instance_exec(self, self.class.tasks_lambda)  endend