Rails与多个外键的关联
我希望能够在一张桌子上使用两列来定义关系。 所以以任务应用程序为例。
尝试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
那么然后Task.create(owner_id:1, assignee_id: 2)
这允许我执行返回用户一的 Task.first.assignee
和返回用户二的 Task.first.assignee
,但User.first.task
返回任何内容。 这是因为任务不属于用户,他们属于所有者和受让人 。 所以,
尝试2:
class User < ActiveRecord::Base has_many :tasks, foreign_key: [:owner_id, :assignee_id] end class Task < ActiveRecord::Base belongs_to :user end
这完全失败了,因为两个外键似乎不被支持。
所以我想要的是能够说User.tasks
并获得用户拥有和分配的任务。
基本上以某种方式build立一个关系,将等于Task.where(owner_id || assignee_id == 1)
那可能吗?
更新
我不想使用finder_sql
,但这个问题的不被接受的答案看起来接近我想要的: Rails – 多索引密钥关联
所以这个方法看起来像这样,
尝试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
虽然我可以得到它在Rails 4
工作,我不断收到以下错误:
ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id
TL; DR
class User < ActiveRecord::Base def tasks Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id) end end
删除User
类中的has_many :tasks
。
使用has_many :tasks
完全没有意义,因为我们在表tasks
没有任何名为user_id
列。
我所做的解决这个问题在我的情况是:
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" 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" # 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
这样,您可以调用User.first.assigned_tasks
以及User.first.owned_tasks
。
现在,您可以定义一个名为tasks
的方法,返回assigned_tasks
和owned_tasks
的组合。
这可能是一个很好的解决scheme,但是从性能的angular度来看,现在并没有那么好,为了获得tasks
,两个查询将被发出而不是一次,然后,这两个查询的结果也需要join。
因此,为了获得属于用户的任务,我们将按照以下方式在User
类中定义一个自定义tasks
方法:
def tasks Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id) end
这样,它将在单个查询中获取所有结果,而不必合并或合并任何结果。
Rails 5:
如果你还想要一个has_many关联,你需要查看默认的where子句,请参阅@Dwight答案。
虽然User.joins(:tasks)
给了我
ArgumentError: The association scope 'tasks' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.
由于这是不可能的,你也可以使用@Arslan Ali解决scheme。
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:关于@JonathanSimmons评论
必须将用户对象传递到用户模型的作用域看起来像是一种倒退的方法
您不必将用户模型传递给此范围。 当前用户实例自动传递给这个lambda。 像这样调用它:
user = User.find(9001) user.tasks
UPDATE2:
如果可能的话,你可以扩大这个答案来解释发生了什么? 我想更好地理解,所以我可以实现类似的东西。 谢谢
调用has_many :tasks
ActiveRecord类中的has_many :tasks
将在某个类variables中存储一个lambda函数,并且只是在其对象上生成一个tasks
方法的一种奇特方法,该方法将调用该lambda函数。 生成的方法看起来类似于下面的伪代码:
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) end end
我为此制定了一个解决scheme。 我愿意接受任何关于如何使这一点变得更好的指示。
class User < ActiveRecord::Base def tasks Task.by_person(self.id) end end class Task < ActiveRecord::Base scope :completed, -> { where(completed: true) } belongs_to :owner, class_name: "User", foreign_key: "owner_id" belongs_to :assignee, class_name: "User", foreign_key: "assignee_id" def self.by_person(user_id) where("owner_id = :person_id OR assignee_id = :person_id", person_id: user_id) end end
这基本上覆盖has_many关联,但仍然返回我正在寻找的ActiveRecord::Relation
对象。
所以现在我可以做这样的事情:
User.first.tasks.completed
,结果是所有完成的任务拥有或分配给第一个用户。
延伸上面的@ dre-hh的答案,我发现在Rails 5中不再像预期的那样工作了。看来Rails 5现在包含了WHERE tasks.user_id = ?
的默认where子句WHERE tasks.user_id = ?
,由于在这种情况下没有user_id
列,所以失败。
我发现它仍然可以使用has_many
关联,你只需要取消这个由Rails添加的where子句。
class User < ApplicationRecord has_many :tasks, ->(user) { unscope(:where).where("owner_id = :id OR assignee_id = :id", id: user.id) } end
我对rails(3.2)中的Associations和(multiple)外键的回答是:如何在模型中描述它们,写下迁移只是为了你!
至于你的代码,这里是我的修改
class User < ActiveRecord::Base has_many :tasks, ->(user) { unscope(where: :user_id).where("owner_id = ? OR assignee_id = ?", user.id, user.id) }, class_name: 'Task' 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
警告:如果您使用的是RailsAdmin,并且需要创build新logging或编辑现有logging,请不要按照我的build议进行操作。因为这样做会导致问题,例如:
current_user.tasks.build(params)
原因是rails会尝试使用current_user.id来填充task.user_id,只是发现没有什么像user_id。
所以,考虑我的黑客方法,作为一种方式,但不这样做。