【问题标题】:Matching nested model association attribute with includes将嵌套模型关联属性与包含匹配
【发布时间】:2018-12-16 23:11:43
【问题描述】:

假设我有以下模型:

class Post < ActiveRecord::Base
  has_many :authors

class Author < ActiveRecord::Base
  belongs_to :post

假设Author 模型有一个属性name

我想搜索给定作者“alice”的所有帖子,该作者的姓名。假设有另一位作者“bob”与 alice 共同撰写了一篇文章。

如果我使用includeswhere 搜索第一个结果:

post = Post.includes(:authors).where("authors.name" => "alice").first

您会看到该帖子现在只有一位作者,即使实际上还有更多:

post.authors #=> [#<Author id: 1, name: "alice", ...>]
post.reload
post.authors #=> [#<Author id: 1, name: "alice", ...>, #<Author id: 2, name: "bob", ...>]

问题似乎是includeswhere 的组合,它将范围正确地限制为所需的帖子,但同时隐藏了除了匹配的关联之外的所有关联。

我想以ActiveRecord::Relation 结束链接,所以上面的重新加载解决方案并不令人满意。将 includes 替换为 joins 可以解决此问题,但不会急切加载关联:

Post.joins(:authors).where("authors.name" => "alice").first.authors
#=> [#<Author id: 1, name: "alice", ...>, #<Author id: 2, name: "bob", ...>]
Post.joins(:authors).where("authors.name" => "alice").first.authors.loaded?
#=> false

有什么建议吗?提前感谢,我一直在努力解决这个问题。

【问题讨论】:

  • 注意:我意识到帖子/作者关联应该更现实地是 HABTM,但对于本问题而言,它不会改变任何东西。

标签: ruby-on-rails-3 activerecord associations arel active-relation


【解决方案1】:

我看到您正在按照预期的行为进行操作,至少 SQL 是这样工作的...您将作者的联接限制在 authors.id = 1 的位置,那么它为什么要加载任何其他人呢? ActiveRecord 只是获取数据库返回的行,它无法知道是否还有其他行,而不需要根据 post.id 进行另一个查询。

这是一种可能的子查询解决方案,它将作为可链接的关系工作,并在一个查询中执行:

relation = Post.find_by_id(id: Author.where(id:1).select(:post_id))

如果添加包含,您将看到查询以两种方式之一发生:

relation = relation.includes(:authors)

relation.first
# 1. Post Load SELECT DISTINCT `posts`.`id`...
# 2. SQL SELECT `posts`.`id` AS t0_r0, `posts`.`title` AS t0_r1, ...

relation.all.first
# 1. SQL SELECT `posts`.`id` AS t0_r0, `posts`.`title` AS t0_r1, ...

因此,根据场景,ActiveRecord 决定是否在加载所有关联作者之前使用更简单的查询查找 id。有时分两步运行查询更有意义。

【讨论】:

  • 非常感谢,这很有意义。在这种情况下,我仍然发现 rails 的行为不直观,并且不是我认为普通用户所期望的,但也许这是不可避免的。
【解决方案2】:

很久很久之后回到这个问题,我意识到有更好的方法来做到这一点。关键是不要一个而是两个连接,一个是includes,一个是使用表别名的Arel:

posts   = Post.arel_table
authors = Author.arel_table.alias("matching_authors")
join    = posts.join(authors, Arel::Nodes::InnerJoin).
                on(authors[:post_id].eq(posts[:id])).join_sources

post = Post.includes(:authors).joins(join).
            where(matching_authors: { name: "Alice" }).first

这个查询的 SQL 很长,因为它有 includes,但关键是它没有一个,而是 两个 连接,一个(来自 includes)使用 @987654325 @ 在别名 posts_authors 上,另一个(来自 Arel join)在别名 matching_authors 上使用 INNER JOINWHERE 只适用于后一个别名,因此返回结果中的关联结果不受此条件限制。

【讨论】:

    【解决方案3】:

    我遇到了同样的问题(我将其描述为:where 子句在使用 includes 时过滤了 关联 模型,而不是 primary 模型以防止 N+1 查询)。

    在尝试了各种解决方案之后,我发现将preloadjoins 结合使用可以为我解决这个问题。 Rails 文档在这里不是很有用。但显然preload 将明确use two separate queries,一个用于过滤/选择主要模型,第二个查询用于加载相关模型。这个blog post 也有一些帮助我找到解决方案的见解。

    将此应用于您的模型将类似于:

    post = Post.preload(:authors).joins(:authors).where("authors.name" =&gt; "alice").first

    我怀疑这在幕后与your accepted answer 做同样的事情,但抽象程度更高。

    我希望 Rails 文档更明确地说明如何执行此操作。我在我的代码库中围绕这种精确情况编写了一堆测试,这已经足够微妙了。

    【讨论】:

    • 感谢您的评论!这确实是另一种方法。但是,澄清一下,任何涉及多个查询的解决方案都不同于公认的解决方案。公认的解决方案在同一个表上使用两个连接(一个别名),但只有 一个查询。它有点复杂,可能不一定比两个查询解决方案性能更高,但它完成了我最初想做的事情。你说得对,如果 Rails 能更明确地说明如何解决这类问题,那就太好了,因为它必须相当普遍。
    【解决方案4】:

    其实是因为这段代码:

    post = Post.includes(:authors).where("authors.name" => "alice").first
    

    由于“.first”而返回第一个匹配的记录。我想如果你这样做:

    post = Post.includes(:authors).where("authors.name" => "alice")
    

    如果我理解您的要求正确,您将获得与“alice”和她的其他合著者的所有帖子。

    【讨论】:

    • 不,这不是问题所在。查询返回正确的帖子(在本例中是 alice 是作者之一的帖子)。但是当我访问该查询的结果时,返回的帖子上的作者关联(在这种情况下是第一个,但没关系)只有我搜索的作者,即使可能有其他作者。
    猜你喜欢
    • 2014-10-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-08
    相关资源
    最近更新 更多