ПОЛНОЕ НАРУЖНОЕ СОЕДИНЕНИЕ с Rails 6 ActiveRecord - PullRequest
2 голосов
/ 20 марта 2020

Я моделирую приложение с сообщениями и уведомлениями следующим образом (упрощенно):

# 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: ...>

Однако:

  1. Это очень многословно и код необходимо обновлять всякий раз, когда новый атрибут добавляется, например, к Message или Notification;
  2. Я не уверен в производительности (я думаю, что это нормально, поскольку все уже загружено в одном запрос, я не думаю, что, например, существует проблема N + 1);
  3. Самое главное, я бы действительно предпочел добиться того же, используя has_many :notifications связь с User, или, если это невозможно , используя пользовательскую область notifications. Это сделано для того, чтобы избежать энергичной загрузки, иметь более гибкий API, чтобы иметь возможность объединяться с другими возможными отношениями, например, c. Или, по крайней мере, я хотел бы улучшить синтаксис, чтобы он был более похож на Rails.

Есть идеи?

1 Ответ

1 голос
/ 20 марта 2020

Вы можете получить уведомления, которые принадлежат пользователю. Затем получите сообщения, у которых нет уведомления для целевого пользователя, и создайте для них уведомление. Наконец, объедините две коллекции.

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%.

...