I want to be able to use two columns on one table to define a relationship. So using a task app as an example.
Attempt 1:
class User < ActiveRecord::Base
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
end
So then Task.create(owner_id:1, assignee_id: 2)
This allows me to perform Task.first.owner
which returns user one and Task.first.assignee
which returns user two but User.first.task
returns nothing. Which is because task doesn't belong to a user, they belong to owner and assignee. So,
Attempt 2:
class User < ActiveRecord::Base
has_many :tasks, foreign_key: [:owner_id, :assignee_id]
end
class Task < ActiveRecord::Base
belongs_to :user
end
That just fails altogether as two foreign keys don't seem to be supported.
So what I want is to be able to say User.tasks
and get both the users owned and assigned tasks.
Basically somehow build a relationship that would equal a query of Task.where(owner_id || assignee_id == 1)
Is that possible?
Update
I'm not looking to use finder_sql
, but this issue's unaccepted answer looks to be close to what I want: Rails - Multiple Index Key Association
So this method would look like this,
Attempt 3:
class Task < ActiveRecord::Base
def self.by_person(person)
where("assignee_id => :person_id OR owner_id => :person_id", :person_id => person.id
end
end
class Person < ActiveRecord::Base
def tasks
Task.by_person(self)
end
end
Though I can get it to work in Rails 4
, I keep getting the following error:
ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id
I worked out a solution for this. I'm open to any pointers on how I can make this better.
This basically overrides the has_many association but still returns the
ActiveRecord::Relation
object I was looking for.So now I can do something like this:
User.first.tasks.completed
and the result is all completed task owned or assigned to the first user.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 nouser_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.My answer to Associations and (multiple) foreign keys in rails (3.2) : how to describe them in the model, and write up migrations is just for you!
As for your code,here are my modifications
Warning: If you are using RailsAdmin and need to create new record or edit existing record,please don't do what I've suggested.Because this hack will cause problem when you do something like this:
The reason is that rails will try to use current_user.id to fill task.user_id,only to find that there is nothing like user_id.
So,consider my hack method as an way outside the box,but don't do that.
TL;DR
Remove
has_many :tasks
inUser
class.Using
has_many :tasks
doesn't make sense at all as we do not have any column nameduser_id
in tabletasks
.What I did to solve the issue in my case is:
This way, you can call
User.first.assigned_tasks
as well asUser.first.owned_tasks
.Now, you can define a method called
tasks
that returns the combination ofassigned_tasks
andowned_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 inUser
class in the following way:This way, it will fetch all the results in one single query, and we wouldn't have to merge or combine any results.
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 meAs it is no longer possible you can use @Arslan Ali solution as well.
Rails 4:
Update1: Regarding @JonathanSimmons comment
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:
Update2:
Calling
has_many :tasks
on ActiveRecord class will store a lambda function in some class variable and is just a fancy way to generate atasks
method on its object, which will call this lambda. The generated method would look similar to following pseudocode: