модуль 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'
Методы открытого экземпляра
# File activerecord/lib/active_record/delegated_type.rb, line 211 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. - :foreign_type
-
Укажите столбец, используемый для хранения типа связанного объекта. По умолчанию это выводится как переданный
roleс суффиксом «_type». Класс, который определяет ассоциациюdelegated_type :entryable, types: %w[ Message Comment ], будет использовать «entryable_type» в качестве значения по умолчанию:foreign_type. - :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.