使用includes匹配嵌套模型关联属性

时间:2012-02-09 03:33:57

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

假设我有以下型号:

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的组合,它将范围正确地限制到所需的帖子,但同时隐藏了除匹配之外的所有关联。 / p>

我想最后使用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

有什么建议吗?在此先感谢,我一直在讨论这个问题。

4 个答案:

答案 0 :(得分:1)

我看到你正在做什么作为预期的行为,至少SQL是如何工作的......你将作者的连接限制在authors.id = 1的位置,那么为什么它会加载任何其他的? ActiveRecord只接受数据库返回的行,它无法知道是否有其他行,而没有基于posts.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。有时,分两步运行查询会更有意义。

答案 1 :(得分:1)

经过很长一段时间回到这个问题,我意识到有更好的方法可以做到这一点。关键是不要使用两个连接,一个使用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)在别名LEFT OUTER JOIN上使用posts_authors,在别名join上使用INNER JOIN使用matching_authors上的WHEREFROM php:7.2-fpm RUN apt-get update -y RUN apt-get install -y python && \ curl -sSL https://sdk.cloud.google.com | bash ENV PATH $PATH:/root/google-cloud-sdk/bin 仅适用于后一个别名,因此返回结果中关联的结果不受此条件的限制。

答案 2 :(得分:1)

我遇到了同样的问题(我将其描述为:{{1时,where子句会过滤 associated 模型,而不是 primary 模型。 }}用于阻止N + 1个查询)。

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

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

preload

我怀疑在幕后它的作用与your accepted answer相同,但抽象度更高。

我希望Rails文档更明确地说明如何执行此操作。足够微妙的是,我在代码库中针对这种精确情况编写了许多测试。

答案 3 :(得分:-2)

实际上,这是因为这段代码:

post = Post.includes(:authors).where("authors.name" => "alice").first
由于“.first”,

返回第一个匹配的记录。我想如果你这样做了:

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

如果我理解你的正确要求,你会回复所有与“alice”和她的其他合着者的帖子。