Rails与多个外键关联

时间:2014-07-08 21:38:22

标签: ruby-on-rails ruby-on-rails-4 associations model-associations

我希望能够在一个表上使用两列来定义关系。所以使用任务应用程序作为示例。

尝试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返回用户1 Task.first.assignee返回用户2 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

虽然我可以在Rails 4中使用它,但我一直收到以下错误:

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

7 个答案:

答案 0 :(得分:69)

TL; DR

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

删除has_many :tasks课程中的User

使用has_many :tasks完全没有意义,因为我们在表user_id中没有任何名为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_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

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

答案 1 :(得分:24)

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

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

  

必须将用户对象传递到User模型的范围内,这似乎是一种向后的方法

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

user = User.find(9001)
user.tasks

Update2:

  

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

在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

答案 2 :(得分:22)

在上面的@dre -hh的答案中进行了扩展,我发现它在Rails 5中不再正常工作。看来Rails 5现在包含WHERE tasks.user_id = ?效果的默认where子句,由于没有此方案中的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

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

答案 4 :(得分:2)

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

Dimensions:  (index: 10)
Coordinates:
  * index    (index) MultiIndex
  - lat      (index) int64 6 8 5 17 15 16 9 19 11 14
  - lon      (index) int64 15 7 9 17 18 1 12 2 6 8
Data variables:
    dv_v     (index) int64 5 14 6 1 19 12 16 10 0 11
    rxy      (index) int64 15 8 6 2 0 1 4 16 7 19
    rxyz     (index) int64 15 17 18 5 14 13 16 2 10 9
    depth    (index) int64 11 18 5 19 3 14 7 17 0 4

答案 5 :(得分:1)

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

current_user.tasks.build(params)

原因是rails会尝试使用current_user.id来填充task.user_id,但却发现没有像user_id这样的内容。

所以,考虑一下我的黑客方法,但不要这样做。

答案 6 :(得分:0)

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

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