Я думаю, что самый простой способ заставить эту работу работать с PostgreSQL - это использовать представление базы данных для поддержки вашей has_one :latest_comment
ассоциации.Представление базы данных, в большей или меньшей степени, именованный запрос, который действует как таблица только для чтения.
Здесь есть три широких варианта:
- Использовать множество запросов: от одного дополучить сообщения, а затем по одному для каждого сообщения, чтобы получить его последний комментарий.
- Денормализовать последний комментарий в сообщение или его собственную таблицу.
- Использовать оконную функцию дляоткройте последние комментарии из таблицы
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
, и все будет работать.Конечно, это не так просто:
ActiveRecord не понимает представления в миграциях, поэтому вы можете попробовать использовать что-то вроде живописный или переключатель от schema.rb
до structure.sql
.
Создайте представление:
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
, чтобы сделать этот запрос более эффективным, но в любом случае вы сделаете это рано или поздно.
Создайте модель и не забудьтевручную установите первичный ключ или includes
и references
не будут ничего предварительно загружать (но preload
будет):
class LatestComment < ApplicationRecord
self.primary_key = :id
belongs_to :post
end
Упростите существующий has_one
до:
has_one :latest_comment
Возможно, добавьте быстрый тест в ваш набор тестов, чтобы убедиться, что Comment
и LatestComment
имеют одинаковые столбцы.Представление не будет автоматически обновляться при изменении таблицы comments
, но простой тест будет служить напоминанием.
Когда кто-то жалуется на «логику в базе данных», сообщите емувзять их догму в другом месте, так как у вас есть работа.
Только чтобы это не потерялось в комментариях, ваша главная проблема в том, что вы злоупотребляете 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
.