Rails与多个外键的关联


84

我希望能够在一个表上使用两列来定义一种关系。因此,以任务应用为例。

尝试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返回用户1Task.first.assignee返回用户2User.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让用户拥有和分配任务。

基本上以某种方式建立了一个等于查询的关系 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


感谢您提供的信息,但这不是我想要的。我想要查询一个或给定值的列。不是复合主键。
JonathanSimmons

是的,更新使之很清楚。忘掉宝石吧。我们都认为您只想使用组合主键。这至少应该通过定义自定义范围和范围关系来实现。有趣的场景。我稍后再看看一个它
DRE-HH

FWIW,我的目标是获得给定的用户任务并保留ActiveRecord :: Relation格式,以便我可以继续在结果上使用任务范围进行搜索/过滤。
JonathanSimmons

Answers:


80

TL; DR

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

has_many :tasksUser课堂上删除。


使用has_many :tasks根本没有意义,因为我们没有user_id在table中命名任何列tasks

为了解决我的问题,我要做的是:

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,将发出两个查询,而不是一次,然后返回结果这两个查询中的一个也需要加入。

因此,为了获取属于用户的任务,我们将通过以下方式tasksUser类中定义一个自定义方法:

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

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


这个解决方案很棒!这确实意味着需要分别访问分配,拥有和所有任务。在一个大型项目中,某些事情会很有前途。但是,对于我而言,这不是必需的。至于has_many“没有意义”,如果您阅读了接受的答案,您会发现我们最终并没有使用has_many声明。假设您的要求符合其他所有访客的需求,那么该回答可能没有那么多判断力。
JonathanSimmons

话虽这么说,但我确实相信这是我们应该提倡的更好的长期设置。如果您要修改答案以包括其他模型的基本示例以及用于检索组合任务集的用户方法,则将其修改为所选答案。谢谢!
JonathanSimmons

是的,我同意你的看法。使用owned_tasksassigned_tasks获取所有任务都会有性能提示。无论如何,我已经更新了答案,并在User类中包括了一个获取所有关联的方法tasks,它将在一个查询中获取结果,而无需合并/合并。
Arslan Ali

2
仅供参考,Task实际上不需要模型中指定的外键。由于您具有belongs_to名为:owner和的关系:assignee,因此默认情况下,Rails会假定外键分别命名为owner_idassignee_id
jeffdill2

1
@ArslanAli没问题。:-)
jeffdill2

47

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

我发现仍然可以使它与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

@fgblomqvist所属的有一个名为:primary_key的选项,该选项指定返回用于关联的关联对象的主键的方法。默认情况下,这是id。我认为这可以为您提供帮助。
艾哈迈德·卡马尔

@AhmedKamal我根本不知道这有什么用?
fgblomqvist,

24

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解决方案。

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

更新1:关于@JonathanSimmons评论

必须将用户对象传递到用户模型的作用域中,这似乎是一种向后的方法

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

user = User.find(9001)
user.tasks

更新2:

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

调用has_many :tasksActiveRecord类将在一个类变量中存储一个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

1
太棒了,在我看到的所有其他地方,人们一直试图建议使用这种过时的宝石作为复合钥匙
FireDragon 2015年

2
如果可能的话,您能否扩展此答案以解释正在发生的事情?我想更好地理解它,以便可以实现类似的功能。谢谢
Stephen Lead

1
尽管这保留了关联,但是会引起其他问题。来自docs:注意:这些关联的加入,渴望加载和预加载是不可能的。这些操作发生在实例创建之前,并且将使用nil参数调用作用域。这可能会导致意外行为,因此不建议使用。 api.rubyonrails.org/classes/ActiveRecord/Associations/…–
s2t2

1
这看起来很有希望,但是Rails 5最终仍然使用带where查询的foreign_key而不是仅使用上面的lambda
Mohamed El Mahallawy

1
是的,看来这不再适用于Rails5。–
Dwight

13

我为此解决了一个问题。我对如何使它变得更好持开放态度。

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 结果是所有已完成的任务拥有或分配给第一个用户。


您是否仍在使用这种方法来解决您的问题?我在同一条船上,想知道您是否学会了一种新方法,还是这仍然是最佳选择。
2014年

仍然是我找到的最佳选择。
JonathanSimmons

那可能是旧尝试的残余。编辑答案以将其删除。
JonathanSimmons

1
这和下面的dre-hh答案会完成同样的事情吗?
dkniffin

1
>必须将用户对象传递到用户模型的范围内,这似乎是一种向后的方法。您无需传递任何信息,这是一个lambda,当前用户实例会自动传递给它
dre-hh 2015年

2

我对Rails(3.2)中的Associations和(多个)外键的回答:如何在模型中描述它们并编写迁移仅适用于您!

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

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,并且需要创建新记录或编辑现有记录,请不要执行我建议的操作。因为这样做会导致以下问题:

current_user.tasks.build(params)

原因是Rails会尝试使用current_user.id填充task.user_id,只是发现没有类似于user_id的东西。

因此,请将我的hack方法视为开箱即用的方法,但不要那样做。


不确定此答案是否提供了已批准答案的所有内容。而且,感觉就像是在问另一个问题的广告。
乔纳森·

@JonathanSimmons谢谢。如果无法像广告一样有效,我将删除此答案。
sunsoft

2

从Rails 5开始,您还可以执行ActiveRecord更安全的方法:

def tasks
  Task.where(owner: self).or(Task.where(assignee: self))
end
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.