假设我有以下型号:
class Post < ActiveRecord::Base
has_many :authors
class Author < ActiveRecord::Base
belongs_to :post
假设Author
模型具有属性name
。
我想通过该作者的名字搜索给定作者“alice”的所有帖子。假设有另一位作者“bob”与alice共同撰写了一篇文章。
如果我使用includes
和where
搜索第一个结果:
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", ...>]
问题似乎是includes
和where
的组合,它将范围正确地限制到所需的帖子,但同时隐藏了除匹配之外的所有关联。 / 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
有什么建议吗?在此先感谢,我一直在讨论这个问题。
答案 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
上的WHERE
。 FROM 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个查询)。
在尝试各种解决方案后,我发现结合使用preload和includes
可以为我解决这个问题。 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”和她的其他合着者的帖子。