Rails与多个外键关联

Jon*_*ons 74 ruby-on-rails associations model-associations ruby-on-rails-4

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

尝试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
Run Code Online (Sandbox Code Playgroud)

那么 Task.create(owner_id:1, assignee_id: 2)

这允许我执行Task.first.owner返回用户1Task.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
Run Code Online (Sandbox Code Playgroud)

这完全失败,因为似乎没有支持两个外键.

所以我想要的是能够说出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
Run Code Online (Sandbox Code Playgroud)

虽然我可以让它工作Rails 4,但我不断收到以下错误:

ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id
Run Code Online (Sandbox Code Playgroud)

Ars*_*Ali 74

TL; DR

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

has_many :tasksUser课堂上删除.


使用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
Run Code Online (Sandbox Code Playgroud)

通过这种方式,你可以调用User.first.assigned_tasks以及User.first.owned_tasks.

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

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

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

def tasks
  Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
end
Run Code Online (Sandbox Code Playgroud)

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

  • 仅仅是一个FYI,`Task`模型中指定的外键实际上不是必需的.由于你有`belongs_to`关系名为`:owner`和`:assignee`,Rails默认假设外键分别命名为`owner_id`和`assignee_id`. (2认同)

Dwi*_*ght 33

在扩展@ 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
Run Code Online (Sandbox Code Playgroud)


dre*_*-hh 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.
Run Code Online (Sandbox Code Playgroud)

由于不再可能,您也可以使用@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
Run Code Online (Sandbox Code Playgroud)

Update1:​​关于@JonathanSimmons评论

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

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

user = User.find(9001)
user.tasks
Run Code Online (Sandbox Code Playgroud)

UPDATE2:

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

调用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
Run Code Online (Sandbox Code Playgroud)

  • 如果可能的话,你可以扩展这个答案来解释发生了什么吗?我想更好地理解它,所以我可以实现类似的东西.谢谢 (2认同)
  • 虽然这保留了关联,但它会导致其他问题。来自文档: **注意:这些关联的加入、预加载和预加载并不完全可能。这些操作发生在实例创建之前,并且将使用 nil 参数调用作用域。这可能会导致意外行为,因此已被弃用。** http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Accessing+the+owner+目的 (2认同)

Jon*_*ons 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
Run Code Online (Sandbox Code Playgroud)

这基本上覆盖了has_many关联,但仍然返回ActiveRecord::Relation我正在寻找的对象.

所以现在我可以这样做:

User.first.tasks.completed 结果是所有已完成的任务拥有或分配给第一个用户.