Spec-Zone.ru › Ruby on Rails 7.0

модуль ActiveRecord::DelegatedType

Делегированные типы

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

Проблема с чисто абстрактными классами заключается в том, что все конкретные подклассы должны сохранять все общие атрибуты сами в своих собственных таблицах (также известное как наследование по классам). Это затрудняет выполнение запросов по всей иерархии. Например, представьте себе следующую иерархию:

Entry < ApplicationRecord
Message < Entry
Comment < Entry

Как отобразить ленту, которая содержит записи Message и Comment, которые легко можно постраничивать? Ну, вы не можете! Сообщения хранятся в таблице сообщений, а комментарии — в таблице комментариев. Вы не можете одновременно извлекать данные из обеих таблиц и использовать согласованную схему OFFSET/LIMIT.

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

Но есть и третий способ: делегированные типы. В этом подходе «суперкласс» — это конкретный класс, который представлен собственной таблицей, где хранятся все атрибуты суперкласса, общие для всех «подклассов». А затем у каждого подкласса есть свои собственные отдельные таблицы для дополнительных атрибутов, специфичных для их реализации. Это похоже на то, что называется наследованием по нескольким таблицам в Django, но вместо фактического наследования этот подход использует делегирование для формирования иерархии и распределения обязанностей.

Рассмотрим пример с записями/сообщениями/комментариями, используя делегированные типы:

# Schema: entries[ id, account_id, creator_id, created_at, updated_at, entryable_type, entryable_id ]
class Entry < ApplicationRecord
  belongs_to :account
  belongs_to :creator
  delegated_type :entryable, types: %w[ Message Comment ]
end

module Entryable
  extend ActiveSupport::Concern

  included do
    has_one :entry, as: :entryable, touch: true
  end
end

# Schema: messages[ id, subject, body ]
class Message < ApplicationRecord
  include Entryable
end

# Schema: comments[ id, content ]
class Comment < ApplicationRecord
  include Entryable
end

Как видите, ни Message ни Comment не предназначены для самостоятельного использования. Ключевые метаданные для обоих классов хранятся в «суперклассе» Entry. Но Entry абсолютно может существовать автономно с точки зрения возможностей запросов, в частности. Теперь вы можете легко выполнять такие действия, как:

Account.find(1).entries.order(created_at: :desc).limit(50)

Это именно то, что нужно для отображения сообщений и комментариев вместе. Саму запись можно легко отобразить как ее делегированный тип, например:

# entries/_entry.html.erb
<%= render "entries/entryables/#{entry.entryable_name}", entry: entry %>

# entries/entryables/_message.html.erb
<div class="message">
  <div class="subject"><%= entry.message.subject %></div>
  <p><%= entry.message.body %></p>
  <i>Posted on <%= entry.created_at %> by <%= entry.creator.name %></i>
</div>

# entries/entryables/_comment.html.erb
<div class="comment">
  <%= entry.creator.name %> said: <%= entry.comment.content %>
</div>

Наследование поведения с помощью модулей и контроллеров

«Суперкласс» записи также идеально подходит для размещения всей общей логики, применимой к сообщениям и комментариям, и которая действует в первую очередь на общих атрибутах. Представьте:

class Entry < ApplicationRecord
  include Eventable, Forwardable, Redeliverable
end

Это позволяет иметь контроллеры для таких операций, как ForwardsController и RedeliverableController, которые оба действуют над записями и тем самым предоставляют общие функциональные возможности сообщениям и комментариям.

Создание новых записей

Вы создаёте новую запись, использующую делегирование, создавая делегирующий и делегируемый объекты одновременно, как показано ниже:

Entry.create! entryable: Comment.new(content: "Hello!"), creator: Current.user

Если вам нужна более сложная композиция или вам необходимо выполнить зависимую валидацию, вы должны создать фабричный метод или класс, чтобы обработать сложные потребности. Это может быть так же просто, как:

class Entry < ApplicationRecord
  def self.create_with_comment(content, creator: Current.user)
    create! entryable: Comment.new(content: content), creator: creator
  end
end

Добавление дополнительного делегирования

Делегированный тип не должен просто отвечать на вопрос о том, как называется базовый класс. На самом деле, это, как правило, антипаттерн. Вы строите эту иерархию для использования полиморфизма. Вот простой пример:

class Entry < ApplicationRecord
  delegated_type :entryable, types: %w[ Message Comment ]
  delegate :title, to: :entryable
end

class Message < ApplicationRecord
  def title
    subject
  end
end

class Comment < ApplicationRecord
  def title
    content.truncate(20)
  end
end

Теперь вы можете отобразить набор записей, вызвать +Entry#title+, и полиморфизм предоставит вам ответ.

Вложенные Attributes

Включение вложенных атрибутов для ассоциации delegated_type позволяет создать запись и сообщение за один раз:

class Entry < ApplicationRecord
  delegated_type :entryable, types: %w[ Message Comment ]
  accepts_nested_attributes_for :entryable
end

params = { entry: { entryable_type: 'Message', entryable_attributes: { subject: 'Smiling' } } }
entry = Entry.create(params[:entry])
entry.entryable.id # => 2
entry.entryable.subject # => 'Smiling'

Публичные методы экземпляра

delegated_type(role, types:, **options) Показать исходный код
# File activerecord/lib/active_record/delegated_type.rb, line 206
def delegated_type(role, types:, **options)
  belongs_to role, options.delete(:scope), **options.merge(polymorphic: true)
  define_delegated_type_methods role, types: types, options: options
end

Определяет этот класс как класс, который будет делегировать свой тип для переданного role к ссылкам на классы в types. Это создаст полиморфную belongs_to связь с этим role, а также добавит все удобные методы делегированных типов:

class Entry < ApplicationRecord
  delegated_type :entryable, types: %w[ Message Comment ], dependent: :destroy
end

Entry#entryable_class # => +Message+ or +Comment+
Entry#entryable_name  # => "message" or "comment"
Entry.messages        # => Entry.where(entryable_type: "Message")
Entry#message?        # => true when entryable_type == "Message"
Entry#message         # => returns the message record, when entryable_type == "Message", otherwise nil
Entry#message_id      # => returns entryable_id, when entryable_type == "Message", otherwise nil
Entry.comments        # => Entry.where(entryable_type: "Comment")
Entry#comment?        # => true when entryable_type == "Comment"
Entry#comment         # => returns the comment record, when entryable_type == "Comment", otherwise nil
Entry#comment_id      # => returns entryable_id, when entryable_type == "Comment", otherwise nil

Вы также можете объявлять типизированные пространства имен:

class Entry < ApplicationRecord
  delegated_type :entryable, types: %w[ Message Comment Access::NoticeMessage ], dependent: :destroy
end

Entry.access_notice_messages
entry.access_notice_message
entry.access_notice_message?

Параметры

Параметры options передаются непосредственно в вызов belongs_to, поэтому здесь вы объявляете dependent и т. д. Следующие параметры могут быть включены для специализации поведения удобных методов делегированных типов.

:foreign_key

Укажите внешний ключ, используемый для удобных методов. По умолчанию он определяется из переданного role с суффиксом «_id». Таким образом, класс, который определяет ассоциацию delegated_type :entryable, types: %w[ Message Comment ], будет использовать «entryable_id» в качестве значения по умолчанию :foreign_key.

:primary_key

Укажите метод, который возвращает первичный ключ связанного объекта, используемый для удобных методов. По умолчанию это id.

Примеры параметров:

class Entry < ApplicationRecord
  delegated_type :entryable, types: %w[ Message Comment ], primary_key: :uuid, foreign_key: :entryable_uuid
end

Entry#message_uuid      # => returns entryable_uuid, when entryable_type == "Message", otherwise nil
Entry#comment_uuid      # => returns entryable_uuid, when entryable_type == "Comment", otherwise nil

© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API