【发布时间】:2020-07-01 20:18:37
【问题描述】:
我正在为一个带有消息和通知的应用程序建模,如下所示(简化):
# 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
在这个简化的例子中:
- 消息表保存实际消息(例如标题、文本、图像...)
- 通知表会跟踪哪个用户阅读了哪条消息
- 所有消息都针对所有用户(例如系统范围的“公告”)。概括为让消息针对特定用户。
我正在寻找最简洁(最类似于 Rails)和最高效的方式来加载用户的通知和相关消息,同时确保即使尚未在数据库中为此用户创建任何通知,也能加载所有消息。这是为了避免每次发布新消息时都需要在 DB 中创建与用户数量一样多的通知行。仅当用户已阅读消息(存储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,receiver_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 问题);
- 最重要的是,我真的更愿意在
User上使用has_many :notifications关联来实现相同的目的,或者如果不可能,使用自定义notifications范围。这是为了避免急切加载,拥有更流畅的 API,能够加入其他可能的关系等。或者至少我想以某种方式改进语法,使其更像 Rails。
有什么想法吗?
【问题讨论】:
-
为什么不直接从消息中获取所有行并包含/急切加载带有用户 ID 条件的通知?包含和急切加载都进行外连接,因此即使在通知中没有找到行,它们也会从消息中返回行。如果您想要的是消息列表以及用户是否已阅读它们的指示,那感觉就像您从错误的方向开始。
-
听起来通知真的是
read_receipts。通常,这些在大多数系统中的停留时间不会超过 30 天,这对性能有帮助吗? -
@max 谢谢,我尝试将
has_many :notifications添加到Message,但随后Message.left_outer_joins(:notifications).where(notifications: { recipient_id: 21 })返回一个空结果。此外,从消息端加载是一个好主意,但我仍然没有完全决定建模。我可能会将消息信息保留在 Notification 模型中,并将 Message 重命名为 Announcement,例如 github.com/excid3/jumpstart/tree/master/app/models。这是为了更灵活地处理不针对所有用户而仅针对某些用户的消息。所以无论如何我都会对这两种情况的解决方案感兴趣。 -
@Anthony 谢谢,确实可以选择以这样一种方式加载数据会更好,即自动将超过 X 天的消息显示为已读,而无需创建相应的通知(或 @987654337 @正如你提议的那样称呼他们)。
-
我意识到包含/急切的加载并不能真正起作用,所以我会重新措辞。您想要的是选择消息中的行并选择
notifications.read_at。有很多方法可以做到这一点,例如子查询:Message.left_joins(:notifications).select('messages.*', ['(SELECT 1 FROM notifications n WHERE id.message_id = messages.id AND n.user_id = ?) AS read_by_user', user.id])。在 Postgres 上,您可以使用横向连接。
标签: ruby-on-rails ruby activerecord outer-join full-outer-join