Rails 与多个外键的关联

Posted

技术标签:

【中文标题】Rails 与多个外键的关联【英文标题】:Rails association with multiple foreign keys 【发布时间】:2014-08-29 18:57:13 【问题描述】:

我希望能够使用一张表上的两列来定义关系。因此以任务应用程序为例。

尝试 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.owner 返回用户一Task.first.assignee 返回用户二User.first.task 什么也不返回。这是因为任务不属于用户,它们属于 ownerassignee。所以,

尝试 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 并获得用户拥有和分配的任务。

基本上以某种方式建立一个等于Task.where(owner_id || assignee_id == 1)查询的关系

这可能吗?

更新

我不想使用finder_sql,但这个问题的未接受答案看起来接近我想要的:Rails - Multiple Index Key Association

所以这个方法看起来像这样,

尝试 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

【问题讨论】:

这颗宝石是您要找的吗? github.com/composite-primary-keys/composite_primary_keys 感谢您的信息,但这不是我要找的。我想要查询任何一个或列是给定值。不是复合主键。 是的,更新说明了这一点。忘记宝石。我们都认为您只想使用组合主键。至少通过将自定义范围定义为范围关系,这应该是可能的。有趣的场景。我稍后再看看 FWIW 我的目标是获取给定的用户任务并保留 ActiveRecord::Relation 格式,以便我可以继续对结果使用任务范围进行搜索/过滤。 【参考方案1】:

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_tasksUser.first.owned_tasks

现在,您可以定义一个名为 tasks 的方法,该方法返回 assigned_tasksowned_tasks 的组合。

就可读性而言,这可能是一个很好的解决方案,但从性能的角度来看,它不会像现在那么好,为了获得tasks,将发出两个查询而不是一次,然后,这两个查询的结果也需要连接起来。

因此,为了获取属于某个用户的任务,我们将在User 类中定义一个自定义的tasks 方法,如下所示:

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

这样,它将在一个查询中获取所有结果,我们不必合并或组合任何结果。

【讨论】:

这个解决方案很棒!它确实意味着需要分别访问分配、拥有和所有任务。对于大型项目来说,这将是很好的预见。然而,在我的情况下,这不是一个要求。至于has_many“没有意义”如果您阅读接受的答案,您会发现我们最终没有使用has_many 声明。假设您的要求与其他所有访问者的需求相匹配是没有效率的,那么这个答案可能会少一些评判。 话虽如此,我确实相信这是我们应该向有此问题的人提倡的更好的长期设置。如果您要修改答案以包含其他模型的基本示例和检索组合任务集的用户方法,我会将其修改为选定的答案。谢谢! 是的,我同意你的看法。使用owned_tasksassigned_tasks 获取所有任务会有性能提示。无论如何,我已经更新了我的答案,并在User 类中包含了一个方法来获取所有关联的tasks,它将在一个查询中获取结果,并且不需要进行合并/组合。 仅供参考,Task 模型中指定的外键实际上不是必需的。由于您有名为 :owner:assigneebelongs_to 关系,Rails 将默认假定外键分别命名为 owner_idassignee_id @ArslanAli 没问题。 :-)【参考方案2】:

扩展上面@dre-hh 的答案,我发现它在Rails 5 中不再按预期工作。看来Rails 5 现在包含一个默认的where 子句,其效果为WHERE tasks.user_id = ?,由于没有@987654322 而失败在这种情况下@列。

我发现它仍然可以使用 has_many 关联,您只需取消 Rails 添加的这个附加 where 子句的范围。

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

【讨论】:

谢谢@Dwight,我从来没有想过这个! 不能让它为 belongs_to 工作(父级没有 id,所以它必须基于多列 PK)。它只是说关系为零(看着控制台,我看不到正在执行的 lambda 查询)。 @fgblomqvist belongs_to 有一个名为 :primary_key 的选项,它指定返回用于关联的关联对象的主键的方法。默认情况下,这是 id。我认为这可以帮助你。 @AhmedKamal 我不明白这有什么用? @dwight 你有没有测试过这个解决方案?我的意思是,一切都像一个魅力,但是在运行 expect(described_class.new).to(have_many(:tasks)) 之后我得到 Task does not have a user_id foreign key 错误【参考方案3】:

导轨 5:

您需要取消默认 where 子句的范围 如果您仍然想要 has_many 关联,请参阅@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 解决方案。

导轨 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

更新1: 关于@JonathanSimmons 的评论

必须将用户对象传递到用户模型的作用域中似乎是一种倒退的方法

您不必将用户模型传递到此范围。 当前用户实例会自动传递给此 lambda。 像这样称呼它:

user = User.find(9001)
user.tasks

更新2:

如果可能,您能否扩展此答案以解释发生了什么?我想更好地理解它,以便我可以实现类似的东西。谢谢

在 ActiveRecord 类上调用 has_many :tasks 会将 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

【讨论】:

太棒了,在我看到的其他地方,人们一直试图建议将这个过时的 gem 用于复合键 如果可能,您能否扩展此答案以解释发生了什么?我想更好地理解它,以便我可以实现类似的东西。谢谢 虽然这保留了关联,但它会导致其他问题。来自文档:注意:加入、预加载和预加载这些关联是不完全可能的。这些操作发生在实例创建之前,并且将使用 nil 参数调用范围。这可能会导致意外行为,因此已被弃用。 api.rubyonrails.org/classes/ActiveRecord/Associations/… 这看起来很有希望,但 Rails 5 最终仍然使用带有 where 查询的 foreign_key 而不是只使用上面的 lambda 是的,这似乎不再适用于 Rails 5。【参考方案4】:

我为此制定了解决方案。我愿意接受任何有关如何使这更好的指示。

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 的回答会完成同样的事情吗? > 必须将用户对象传递到 User 模型的作用域中似乎是一种倒退的方法。你不必传递任何东西,这是一个 lambda,当前用户实例会自动传递给它【参考方案5】:

我对@9​​87654321@ 的回答只适合你!

至于你的代码,这是我的修改

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 并且需要创建新记录或编辑现有记录,请不要按照我的建议进行操作。因为当您执行以下操作时,此 hack 会导致问题:

current_user.tasks.build(params)

原因是rails会尝试用current_user.id来填充task.user_id,结果发现没有us​​er_id这样的东西。

所以,将我的 hack 方法视为一种跳出框框的方法,但不要那样做。

【讨论】:

不确定此答案是否提供了已批准答案尚未提供的任何内容。而且,感觉就像是在为另一个问题做广告。 @JonathanSimmons 谢谢。如果它像广告一样不好,我会删除这个答案。【参考方案6】:

从 Rails 5 开始,您也可以使用 ActiveRecord 更安全的方式:

def tasks
  Task.where(owner: self).or(Task.where(assignee: self))
end

【讨论】:

【参考方案7】:

更好的方法是使用多态关联:

task.rb

class Task < ActiveRecord::Base
  belongs_to :taskable, polymorphic: true
end

assigned_task.rb

class AssignedTask < Task
end

owned_task.rb

class OwnedTask < Task
end

user.rb

class User < ActiveRecord::Base
  has_many :assigned_tasks, as: :taskable, dependent: :destroy
  has_many :owned_tasks,    as: :taskable, dependent: :destroy
end

结果,我们可以这样使用它:

new_user = User.create(...)
AssignedTask.create(taskable: new_user, ...)
OwnedTask.create(taskable: new_user, ...)

pp user.assigned_tasks
pp user.owned_tasks

【讨论】:

以上是关于Rails 与多个外键的关联的主要内容,如果未能解决你的问题,请参考以下文章

同一列上具有多个外键的实体框架核心

多表关联

与 Rails 的多个关联已审核

mysql表中,表的外键关联自身主键,为啥插入不了数据?

Laravel 与复合外键的关系

如何在rails 5中为现有模型添加外键