Ассоциация has_one не работает с включает - PullRequest
0 голосов
/ 01 июня 2018

Я пытался выяснить какое-то странное поведение при объединении ассоциации has_one и includes.

class Post < ApplicationRecord
  has_many :comments

  has_one :latest_comment, -> { order('comments.id DESC').limit(1) }, class_name: 'Comment'
end

class Comment < ApplicationRecord
  belongs_to :post
end

Чтобы проверить это, я создал два поста с двумя комментариями в каждом.Вот некоторые команды консоли rails, которые показывают странное поведение.Когда мы используем includes, тогда игнорируется порядок ассоциации * 1007. *.

posts = Post.includes(:latest_comment).references(:latest_comment)
posts.map {|p| p.latest_comment.id}
=> [1, 3]

posts.map {|p| p.comments.last.id}
=> [2, 4]

Я бы ожидал, что эти команды будут иметь одинаковый вывод.posts.map {|p| p.latest_comment.id} должен вернуть [2, 4].Я не могу использовать вторую команду из-за проблем с n + 1 запросом.

Если вы вызываете последний комментарий по отдельности (аналогично comments.last выше), тогда все работает как положено.

[Post.first.latest_comment.id, Post.last.latest_comment.id]
 => [2, 4]

Если у вас есть другой способ добиться такого поведения, я бы приветствовал этот вклад.Этот сбивает меня с толку.

1 Ответ

0 голосов
/ 02 июня 2018

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

Здесь есть три широких варианта:

  1. Использовать множество запросов: от одного дополучить сообщения, а затем по одному для каждого сообщения, чтобы получить его последний комментарий.
  2. Денормализовать последний комментарий в сообщение или его собственную таблицу.
  3. Использовать оконную функцию дляоткройте последние комментарии из таблицы comments.

(1) - это то, чего мы пытаемся избежать. (2) имеет тенденцию приводить к каскаду чрезмерных осложнений и ошибок. (3) хорошо, потому что позволяет базе данных делать то, что она делает хорошо (управлять и запрашивать данные), но ActiveRecord имеет ограниченное понимание SQL, поэтому для ее работы требуется немного дополнительных механизмов.

Мы можем использовать оконную функцию row_number, чтобы найти последний комментарий к сообщению:

select *
from (
  select comments.*,
         row_number() over (partition by post_id order by created_at desc) as rn
  from comments
) dt
where dt.rn = 1

Поиграйте с внутренним запросом в psql, и вы должны увидеть, что делает row_number().

Если мы обернем этот запрос в latest_comments представление и поместим модель LatestComment перед ним, вы можете has_one :latest_comment, и все будет работать.Конечно, это не так просто:

  1. ActiveRecord не понимает представления в миграциях, поэтому вы можете попробовать использовать что-то вроде живописный или переключатель от schema.rb до structure.sql.

  2. Создайте представление:

    class CreateLatestComments < ActiveRecord::Migration[5.2]
      def up
        connection.execute(%q(
          create view latest_comments (id, post_id, created_at, ...) as
          select id, post_id, created_at, ...
          from (
            select id, post_id, created_at, ...,
                   row_number() over (partition by post_id order by created_at desc) as rn
            from comments
          ) dt
          where dt. rn = 1
        ))
      end
      def down
        connection.execute('drop view latest_comments')
      end
    end
    

    Это будет больше похоже на обычную миграцию Rails, если вы 'Вы используете сценическое.Я не знаю структуру вашей comments таблицы, поэтому все ... s там;Вы можете использовать select *, если хотите, и не обращайте внимания на бланк rn в вашем LatestComment.Возможно, вы захотите просмотреть свои индексы на comments, чтобы сделать этот запрос более эффективным, но в любом случае вы сделаете это рано или поздно.

  3. Создайте модель и не забудьтевручную установите первичный ключ или includes и references не будут ничего предварительно загружать (но preload будет):

    class LatestComment < ApplicationRecord
      self.primary_key = :id
      belongs_to :post
    end
    
  4. Упростите существующий has_one до:

    has_one :latest_comment
    
  5. Возможно, добавьте быстрый тест в ваш набор тестов, чтобы убедиться, что Comment и LatestComment имеют одинаковые столбцы.Представление не будет автоматически обновляться при изменении таблицы comments, но простой тест будет служить напоминанием.

  6. Когда кто-то жалуется на «логику в базе данных», сообщите емувзять их догму в другом месте, так как у вас есть работа.


Только чтобы это не потерялось в комментариях, ваша главная проблема в том, что вы злоупотребляете scope аргумент в has_one ассоциации.Когда вы говорите что-то вроде этого:

Post.includes(:latest_comment).references(:latest_comment)

аргумент scope для has_one заканчивается в условии соединения LEFT JOIN, которое includes и references добавляют к запросу.ORDER BY не имеет смысла в условии соединения, поэтому ActiveRecord не включает его, и ваша связь распадается.Вы не можете сделать область видимости зависимой (то есть ->(post) { some_query_with_post_in_a_where... }), чтобы получить предложение WHERE в условие соединения, тогда ActiveRecord выдаст вам ArgumentError, потому что ActiveRecord не знает, как использовать область видимости, зависящую от экземпляра, сincludes и references.

...