具有Rails 6 ActiveRecord的完全外部联接

问题描述 投票:1回答:1

我正在使用以下消息和通知对应用程序进行建模(简化):

# 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

例如,对于用户21和消息1,我有一个ID为1的消息,两个ID为20和ID = 21的用户,一个通知为(ID = 30,message_id = 1,receiver_id = 20),收到带有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: ...>

但是:

  1. 非常冗长,例如,每当将新属性添加到MessageNotification时,都需要更新代码;
  2. 我不确定性能(我认为这很好,因为所有内容都已在单个查询中加载,例如,我不认为存在N + 1问题);
  3. 最重要的是,我真的更愿意使用has_many :notifications上的User关联来实现相同的目标,或者,如果可能的话,使用自定义notifications范围来实现相同的目标。这是为了避免急于加载,拥有更流畅的API,能够加入其他可能的关系等。或者至少我希望以某种方式将语法改进为更像Rails。

有什么想法吗?

ruby-on-rails ruby activerecord outer-join full-outer-join
1个回答
0
投票

您可以获取属于用户的通知。然后检索没有针对目标用户的通知的消息,并为其建立通知。最后将两个集合合并。

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 = self.notifications.includes(:message).load
    notifications + Message
      .where.not(id: notifications.pluck(:message_id))
      .map { |message| self.notifications.build(message: message) }
  end
end

可悲的是,这仍然会产生数组结果。我不确定是否可以为此建立正常关联。希望其他答案能证明我错了。

我想问你这是否产生正确的结果?这个问题非常复杂,我不确定我的理解是否100%正确。

© www.soinside.com 2019 - 2024. All rights reserved.