Я моделирую приложение с сообщениями и уведомлениями следующим образом (упрощенно):
# db/schema.rb
create_table :messages do |t|
...
end
create_table :notifications do |t|
t.references :message, index: true, foreign_key: true, null: false
t.references :recipient, index: true, foreign_key: { to_table: :users }, null: false
t.datetime :read_at
...
end
Модели:
class User < ApplicationRecord; end
class Message < ApplicationRecord; end
class Notification < ApplicationRecord
belongs_to :message
belongs_to :recipient, class_name: 'User'
accepts_nested_attributes_for :message
end
В этом упрощенном примере:
- таблица сообщений содержит фактические сообщения (например, заголовок, текст, изображения, ...)
- таблица уведомлений отслеживает, какой пользователь прочитал, какое сообщение
- все сообщения предназначены для всех пользователи (например, общесистемные «объявления»). Чтобы обобщить, чтобы сообщения предназначались для определенных c пользователей.
Я ищу самый чистый (наиболее похожий на Rails) и наиболее эффективный способ загрузки уведомлений пользователя и связанных сообщений, обеспечивая при этом, чтобы все сообщения загружаются, даже если для этого пользователя в БД еще не было создано никаких уведомлений. Это позволяет избежать необходимости создавать столько строк уведомлений в БД, сколько пользователей существует при каждой публикации нового сообщения. Строка добавляется в таблицу уведомлений только тогда, когда пользователь прочитал сообщение (сохраняя значение read_at
).
Мне удалось добиться этого с помощью этого SQL:
# user.rb
def notifications
unsanitised_sql = <<-SQL
SELECT
:user_id AS recipient_id, m.id as message_id, n.read_at, n.created_at, n.updated_at,
m.text, ...
FROM (
SELECT *
FROM notifications
WHERE recipient_id = :user_id
) n
FULL OUTER JOIN messages m
ON (message_id = m.id)
SQL
ActiveRecord::Base
.connection
.select_all(ActiveRecord::Base.sanitize_sql [ unsanitised_sql, { user_id: id } ])
.map do |row|
Notification.new(
recipient_id: row['recipient_id'],
message_id: row['message_id'],
read_at: row['read_at'],
created_at: row['created_at'],
updated_at: row['updated_at'],
message_attributes: {
id: row['message_id'],
text: row['text'],
...
}
)
end
end
Например, если у меня есть Сообщение с id = 1, два пользователя с id = 20 и id = 21 и одно Уведомление с (id = 30, message_id = 1, receient_id = 20), для пользователя 21 и сообщения 1 я получаю «виртуальное» уведомление с read_at=nil
(так как пользователь еще не прочитал его) и связанные данные сообщения ?
> User.find(21).notifications
=> [#<Notification:0x00007fb5c64df138 id: nil, message_id: 1, recipient_id: 21, read_at: nil, created_at: nil, updated_at: nil>]
> User.find(21).notifications.first.message
=> #<Message:0x00007fd57c52b5a8 id: 1, text: ...>
Однако:
- Это очень многословно и код необходимо обновлять всякий раз, когда новый атрибут добавляется, например, к
Message
или Notification
; - Я не уверен в производительности (я думаю, что это нормально, поскольку все уже загружено в одном запрос, я не думаю, что, например, существует проблема N + 1);
- Самое главное, я бы действительно предпочел добиться того же, используя
has_many :notifications
связь с User
, или, если это невозможно , используя пользовательскую область notifications
. Это сделано для того, чтобы избежать энергичной загрузки, иметь более гибкий API, чтобы иметь возможность объединяться с другими возможными отношениями, например, c. Или, по крайней мере, я хотел бы улучшить синтаксис, чтобы он был более похож на Rails.
Есть идеи?