使用 Rails 6 ActiveRecord 进行完全外连接
Posted
技术标签:
【中文标题】使用 Rails 6 ActiveRecord 进行完全外连接【英文标题】:FULL OUTER JOIN with Rails 6 ActiveRecord 【发布时间】: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 上,您可以使用横向连接。
【参考方案1】:
您可以获取属于该用户的通知。然后检索没有目标用户通知的消息并为他们构建通知。最后合并这两个集合。
class Message < ApplicationRecord
has_many :notifications
end
class Notification < ApplicationRecord
belongs_to :message
belongs_to :recipient, class_name: 'User'
end
class User < ApplicationRecord
has_many :notifications # you might need `inverse_of: :recipient`
def all_notifications
notifications.load + Message
.where.not(id: notifications.pluck(:message_id)).pluck(:id)
.map |message_id| notifications.build(message_id: message_id)
end
end
遗憾的是,这仍然会产生一个数组结果。我不确定这是否有可能建立正常的关联。希望其他答案能证明我错了。
我想问你这是否会产生正确的结果?这个问题很复杂,我不确定我对它的理解是否 100% 正确。
【讨论】:
是的,这解决了第 1 点:代码更加简洁。它不能解决第 2 点:加载所有消息,然后分别加载每条消息的通知。也不是第 3 点:不在 ActiveRecord 关联级别上运行。结果确实看起来正确(与我自己的代码相同)。 我已经更新了答案,这应该在很大程度上解决了第 2 点。现在只执行了 2 个 SQL 语句。第一个获取已经存在的通知。第二个仅检索缺少消息通知的消息 ID。第 3 期仍然有效。以上是关于使用 Rails 6 ActiveRecord 进行完全外连接的主要内容,如果未能解决你的问题,请参考以下文章
将未维护的 ActiveRecord 适配器强制转换为 Rails 版本 6
ActiveRecord Rails将created_at时间戳转换为查询所在的时区
为啥 Rails 5 使用 ApplicationRecord 而不是 ActiveRecord::Base?
Rails使用ActiveRecord Collection或Array更新,导致ActiveRecord :: RecordInvalid错误