6

次のモデルがあるとします。

class Post < ActiveRecord::Base
  has_many :authors

class Author < ActiveRecord::Base
  belongs_to :post

そして、Authorモデルに属性があるとしますname

特定の著者「alice」を含むすべての投稿を、その著者の名前で検索したいと思います。アリスと投稿を共同執筆した別の著者「bob」がいるとします。

includesとを使用して最初の結果を検索する場合where

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

実際にはもっと多くの著者がいる場合でも、投稿には現在1人の著者しかいないことがわかります。

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

includes問題はとの組み合わせにあるようですwhere。これにより、スコープが目的の投稿に正しく制限されますが、同時に、一致するものを除くすべての関連付けが非表示になります。

最終的にはチェーン用になりたい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

4 に答える 4

1

期待どおりの動作をしていることがわかります。少なくとも、SQLの動作方法です...作成者の結合をauthors.id = 1の場所に制限しているのに、なぜ他のユーザーを読み込むのでしょうか。ActiveRecordは、データベースが返した行を取得するだけで、posts.idに基づいて別のクエリを実行しない限り、他の行があるかどうかを知る方法はありません。

サブクエリを使用した1つの可能な解決策を次に示します。これは連鎖可能な関係として機能し、1つのクエリで実行されます。

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

インクルードを追加すると、クエリが次の2つの方法のいずれかで発生することがわかります。

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を検索するかどうかを決定します。2つのステップでクエリを実行する方が理にかなっている場合があります。

于 2012-07-22T11:09:44.427 に答える
1

久しぶりにこの質問に戻ると、これを行うにはもっと良い方法があることに気づきました。重要なのは、テーブルエイリアスを使用して、 1つではなく2つの結合を行うことです。1つはArelを使用し、もう1つは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が、重要な点は、1つではなく2つの結合があり、1つは(からincludesLEFT OUTER JOINエイリアスを使用し、もう1つは( posts_authorsArelから)エイリアスjoinを使用することです。は後者のエイリアスにのみ適用されるため、返される結果の関連付けの結果は、この条件によって制限されません。INNER JOINmatching_authorsWHERE

于 2018-05-27T00:08:17.190 に答える
1

同じ問題が発生しました(N + 1クエリを防ぐために使用される場合、where句はプライマリモデルではなく、関連するモデルをフィルタリングします)。includes

さまざまな解決策を試してみたところ、プリロードを組み合わせて使用​​すると、joinsこれが解決することがわかりました。Railsのドキュメントはここではあまり役に立ちません。しかし、明らかに2つの別々のクエリpreloadを明示的に使用します。1つはプライマリモデルをフィルタリング/選択するためのもので、もう1つは関連するモデルをロードするためのものです。このブログ投稿には、私を解決策に導くのに役立ついくつかの洞察もあります。

これをモデルに適用すると、次のようになります。

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

私は、これがあなたの受け入れられた答えと同じことをしているのではないかと思いますが、より良いレベルの抽象化です。

Railsのドキュメントがこれを行う方法についてより明確になっていることを望みます。コードベースでこの正確な状況を回避するための一連のテストを作成したのは、十分に微妙です。

于 2018-12-16T14:28:01.303 に答える
-2

実際には、これは次のコードが原因です。

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

「.first」のため、最初に一致したレコードを返します。あなたがこれをした場合、私は思います:

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

あなたが正しく質問していることを私が理解していれば、あなたは「alice」と彼女の他の共著者と一緒にすべての投稿を取り戻すでしょう。

于 2012-07-16T23:27:12.913 に答える