6

假设我有以下模型:

class Post < ActiveRecord::Base
  has_many :authors

class Author < ActiveRecord::Base
  belongs_to :post

假设Author模型有一个属性,name

我想以该作者的名字搜索给定作者“爱丽丝”的所有帖子。假设有另一位作者“鲍勃”与爱丽丝合着了一篇文章。

includes如果我使用and搜索第一个结果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", ...>]

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

我想最终得到一个ActiveRecord::Relationfor 链接,所以上面的重新加载解决方案并不令人满意。替换includesjoins解决了这个问题,但并不急于加载关联:

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

4 回答 4

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。有时分两步运行查询更有意义。

于 2012-07-22T11:09:44.427 回答
1

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

在尝试了各种解决方案之后,我发现结合使用preloadjoins可以为我解决这个问题。Rails 文档在这里不是很有用。但显然preload会显式使用两个单独的查询,一个用于过滤/选择主要模型,另一个用于加载关联模型。这篇文也有一些见解,帮助我找到了解决方案。

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

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

我怀疑在幕后,这与您接受的答案相同,但抽象程度更高。

我希望 Rails 文档更明确地说明如何做到这一点。这很微妙,我在我的代码库中围绕这种精确情况编写了一堆测试。

于 2018-12-16T14:28:01.303 回答
1

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

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,但关键是它没有一个而是两个连接,一个(来自includesLEFT OUTER JOIN在 alias 上使用 a posts_authors,另一个(来自 Arel )在 alias 上join使用 an 。仅适用于后一个别名,因此返回结果中的关联结果不受此条件限制。INNER JOINmatching_authorsWHERE

于 2018-05-27T00:08:17.190 回答
-2

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

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

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

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

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

于 2012-07-16T23:27:12.913 回答