Получите последние дочерние сообщения, а также родительские сообщения, которые не имеют детей - PullRequest
1 голос
/ 07 мая 2020

Ниже приводится модель сообщения.

class Message < ApplicationRecord
  belongs_to :parent_message, class_name: 'Message', optional: true
  has_many :child_messages, foreign_key: :parent_message_id, class_name: "Message"
  has_many :message_participants

  scope :latest_messages_by_participant, -> (user_id) do
    select("DISTINCT ON (parent_message_id) messages.*").
        joins(:message_participants).
        where(message_participants: { user_id: user_id }).
        order("parent_message_id, created_at DESC")
  end
end

message_participants имеет запись для каждого сообщения и различных людей, которые отправили или получили это сообщение. У него есть user_id.

Проблема с описанной выше latest_messages_by_participant областью заключается в том, что он может получать все дочерние сообщения, НО он получает только последнее родительское сообщение. Это связано с тем, что мы вызываем DISINTICT ON для parent_message_id, а для бездетных родительских сообщений это значение равно NULL, поэтому он просто вызывает отличное значение NULL и возвращает значение 1 (последнее бездетное родительское сообщение).

Как я могу получить все последние сообщения, включая последние дочерние сообщения И последнее бездетное родительское сообщение в одном запросе?

Я использую Rails 6 и Postgres 11.

PS: Я также должен указать на вторичную проблему, которая заключается в том, что сообщения возвращаются в created_at AS C. Created_at DES C может получить последнее дочернее сообщение, но не сортирует всю коллекцию. Я могу решить эту проблему, вызвав .reverse, но мне интересно, есть ли способ исправить и это.

Ответы [ 2 ]

2 голосов
/ 11 мая 2020

Используйте выражение COALESCE в DISTINCT ON и ORDER BY. И отсортируйте результат во внешнем запросе, чтобы получить желаемый порядок сортировки:

SELECT *
FROM  (
   SELECT DISTINCT ON (COALESCE(m.parent_message_id, m.id))
          m.*
   FROM   messages m
   JOIN   message_participants mp ON ...
   WHERE  mp.user_id = ...
   ORDER  BY (COALESCE(m.parent_message_id, m.id)), created_at DESC
   )
ORDER  BY created_at;

См. (С подробным объяснением):

Производительность?

Для несколько строк на пользователя и идентификатор сообщения, DISTINCT ON обычно является одним из самых быстрых возможных решений. Для много строк есть (гораздо) более быстрые способы. Зависит от дополнительной информации, как указано в комментариях.

2 голосов
/ 11 мая 2020

Я считаю, что вам нужно добавить coalese в свой отдельный on & order by, чтобы выбрать id сообщения, когда parent_message_id имеет значение null.

select("DISTINCT ON (parent_message_id) messages.*")
...
order("parent_message_id, created_at DESC")

необходимо преобразовать в

select("DISTINCT ON (COALESCE(parent_message_id, messages.id)) messages.*")
...
order("COALESCE(parent_message_id, messages.id), created_at DESC")

Итак, вы не предоставили образец таблиц базы данных и ожидаемое или полное определение модели, поэтому я делаю несколько выводов. Вот минимальные определения таблиц (как я их понял), необработанный запрос sql, который будет сгенерирован AR после предложенной мной модификации [это тот запрос, который нам нужен, учитывая схему ниже], и результаты.

Настройка

CREATE TABLE messages (
  id int primary key
, parent_message_id int references messages(id)
, created_at timestamp default current_timestamp
); 
INSERT INTO messages (id, parent_message_id) values 
  (1, NULL) -- parent message with children
, (2, 1)
, (3, 1)
, (4, NULL) -- parent message without children
, (5, NULL) -- another parent message without children
;
CREATE TABLE message_participants (
  user_id int
, message_id int references messages(id)
)
INSERT INTO message_participants values (1, 1), (2, 2), (3, 3), (1, 4), (2, 5);

RAW SQL запрос, который дает нам последнее родительское или дочернее сообщение:

SELECT DISTINCT ON (COALESCE(parent_message_id, messages.id)) messages.*
FROM messages
JOIN message_participants ON message_participants.message_id = messages.id
WHERE message_participants.user_id = ? -- replace by user_id
ORDER BY COALESCE(parent_message_id, messages.id), created_at DESC

Результаты

Учитывая user_id = 1, запрос выше возвращает результат:

 id | parent_message_id |         created_at
----+-------------------+----------------------------
  1 |                   | 2020-05-11 13:50:00.857589
  4 |                   | 2020-05-11 13:50:00.857589
(2 rows)

Учитывая user_id = 2, приведенный выше запрос возвращает результат:

 id | parent_message_id |         created_at
----+-------------------+----------------------------
  2 |                 1 | 2020-05-11 13:50:00.857589
  5 |                   | 2020-05-11 13:52:01.261975
(2 rows)

Сортировка общих результатов:

created_at DES C может получить последнее дочернее сообщение, но не сортирует всю коллекцию. Я могу решить эту проблему, вызвав .reverse, но мне интересно, есть ли способ исправить и это.

Чтобы выполнить сортировку в базе данных, вы можете обернуть вышеуказанный запрос в cte

пример:

WITH last_messages AS (
SELECT DISTINCT ON (COALESCE(parent_message_id, messages.id)) messages.*
FROM messages
JOIN message_participants ON message_participants.message_id = messages.id
WHERE message_participants.user_id = 2
ORDER BY COALESCE(parent_message_id, messages.id), created_at DESC
)
SELECT * FROM last_messages ORDER BY created_at;

Однако я не уверен на 100%, как это будет выражено в AR

...