49

我希望能够在一个表上使用两列来定义关系。因此以任务应用程序为例。具有多个外键的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.owner返回用户一个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并获得用户拥有和分配的任务。

基本上莫名其妙构建应等于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 

虽然我可以得到它在012工作,我不断收到以下错误:

ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id 
+1

这是宝石你在找什么? https://github.com/composite-primary-keys/composite_primary_keys – mus

+0

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

+0

是的,更新说明清楚。忘记宝石。我们都认为你只是想使用组合的主键。这至少应该可以通过定义一个自定义作用域的范围关系来实现。有趣的场景。我将在稍后看一看 –

回答

46

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_tasksowned_tasks的组合。

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

因此,为了获得属于用户的任务,我们将定义一个自定义tasks方法User类以下列方式:

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

通过这种方式,它会取一个所有的结果单个查询,我们不需要合并或合并任何结果。

+0

这个解决方案非常棒!它确实意味着需要分别访问分配,拥有和所有任务。对于大型项目来说,这是一件非常重要的事情。然而这不是我的要求。至于'has_many'“没有意义”如果你阅读接受的答案,你会发现我们并没有最终使用'has_many'声明。假设你的要求与每个其他访问者的需求相匹配都是徒劳无功的,那么这个答案本可以不是一个评判。 – JonathanSimmons

+0

这就是说,我确实认为这是一个更好的长期设置,我们应该提倡有这个问题的人。如果您要修改答案以包含其他模型的基本示例以及用于检索组合任务集的任务的用户方法,我会将其修改为所选答案。谢谢! – JonathanSimmons

+0

是的,我同意你的意见。使用'owned_tasks'和'assigned_tasks'来获得所有任务将会有性能提示。无论如何,我已经更新了我的答案,并且在'User'类中包含了一个方法来获取所有关联的'tasks',它将在一个查询中获取结果,并且不需要进行合并/合并。 –

20

Rails的5:

需要unscope默认的where子句 看到@Dwight答案如果你仍然想的has_many associaiton。

虽然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阿里的解决方案也是如此。

轨道4,5:

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评论

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

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

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

上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 
+1

太棒了,在其他地方,我看着人们一直试图建议使用这个过时的复合钥匙的012m – FireDragon

+2

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

+1

谢谢,这应该是被接受的答案。 – Aeramor

14

我为此制定了解决方案。我愿意接受任何关于如何让这一点变得更好的指示。

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

+0

你还在用这个方法解决你的问题吗?我在同一条船上,想知道你是否学到了新的方法,或者如果这仍然是最好的选择。 – Dan

+0

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

+0

添加has_many关系有没有意义? – Crystark

0

我对Associations and (multiple) foreign keys in rails (3.2) : how to describe them in the model, and write up migrations的回答只为你!

至于你的代码,这里有我的修改

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并需要创建新的记录或编辑现有记录,请不要做什么,我已经suggested.Because这个技巧会造成问题,当你做这样的事情:

current_user.tasks.build(params) 

的原因是铁轨将尝试使用current_user.id填补task.user_id,才发现,有没什么像user_id一样。

所以,考虑我的黑客方法是一种超出框框的方式,但不要这样做。

+0

不确定这个答案是否提供任何经过批准的答案。而且,这感觉就像是另一个问题的广告。 – JonathanSimmons

+0

感谢您的意见,@ JonathanSimmons – sunsoft

8

在@ 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_id = :id OR assignee_id = :id", id: user.id) } 
end 
+0

谢谢!这工作完美! –

+0

谢谢@Dwight,我永远不会想到这个! –