Сопоставление вложенного атрибута ассоциации модели с включенными - PullRequest
5 голосов
/ 09 февраля 2012

Предположим, у меня есть следующие модели:

class Post < ActiveRecord::Base
  has_many :authors

class Author < ActiveRecord::Base
  belongs_to :post

И предположим, что модель Author имеет атрибут, name.

Я хочу найти все сообщения с заданнымАвтор "Алиса", по имени этого автора.Скажем, есть другой автор "Боб", который является соавтором сообщения с Алисой.

Если я ищу первый результат, используя includes и where:

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

You 'Вы увидите, что у поста теперь только один автор, даже если на самом деле их больше:

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 ]

1 голос
/ 16 декабря 2018

Я столкнулся с той же проблемой (которую я описываю как: where предложение фильтрует связанную модель, а не первичную модель, когда includes используется для предотвращенияN + 1 запросов).

После того, как попробовали различные решения, я обнаружил, что использование preload в сочетании с joins решает эту проблему для меня.Документация по Rails здесь не очень полезна.Но, очевидно, preload будет явно использовать два отдельных запроса , один для фильтрации / выбора первичной модели и второй запрос для загрузки связанных моделей.Это сообщение в блоге также содержит некоторые идеи, которые помогли мне привести к решению.

Применение этого к вашим моделям будет выглядеть примерно так:

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

Я подозреваю, что под прикрытием это делает то же самое, что и ваш принятый ответ, но на более хорошем уровне абстракции.

Хотелось бы, чтобы в документации по Rails было более четко указано, как это сделать.Достаточно тонко, что я написал кучу тестов вокруг этой конкретной ситуации в моей кодовой базе.

1 голос
/ 22 июля 2012

Я вижу, что вы делаете, как ожидаемое поведение, по крайней мере, так работает SQL ... Вы ограничиваете объединение авторов, где author.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 решает, нужно ли искать идентификатор с более простым запросом перед загрузкой всех связанных авторов. Иногда имеет смысл выполнить запрос в 2 этапа.

1 голос
/ 27 мая 2018

Возвращаясь к этому вопросу через долгое время, я понял, что есть лучший способ сделать это.Ключ заключается в том, чтобы сделать не одно, а два объединения, одно с 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, другое (из Arel)join) с использованием INNER JOIN на псевдониме matching_authors.WHERE применяется только к последнему псевдониму, поэтому результаты сопоставления в возвращаемых результатах не ограничиваются этим условием.

0 голосов
/ 17 июля 2012

На самом деле, это потому, что этот код:

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

возвращает первую найденную запись из-за ".first".Я думаю, что если бы вы сделали это:

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

, вы бы вернули все сообщения с "Алисой" и другими ее соавторами, если я правильно понимаю, что вы спрашиваете.

...