Spec-Zone.ru › Ruby on Rails 6.1

модуль 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 ]
class Message < ApplicationRecord
  include Entryable
  has_rich_text :content
end

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

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

Account.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">
  Posted on <%= entry.created_at %> by <%= entry.creator.name %>: <%= entry.message.content %>
</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+, и полиморфизм предоставит вам ответ.

Открытые методы экземпляров

delegated_type(role, types:, **options) Показать исходный код
# File activerecord/lib/active_record/delegated_type.rb, line 170
def delegated_type(role, types:, **options)
  belongs_to role, options.delete(:scope), **options.merge(polymorphic: true)
  define_delegated_type_methods role, types: types
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

options передаются напрямую в вызов belongs_to, поэтому здесь вы объявляете dependent и т.д.

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

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?

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

Spec-Zone.ru

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