委托类型
类
层次结构可以以多种方式映射到关系数据库表。例如,Active Record 提供了纯粹的抽象类,其中超类不持久化任何属性,以及单表继承,其中所有层次结构级别上的所有属性都表示在一个表中。两者都有其用途,但都不是没有缺点的。
纯粹抽象类的问题在于,所有具体子类都必须在其自己的表中持久化所有共享属性(也称为类表继承)。这使得跨层次结构进行查询变得困难。例如,假设您有以下层次结构
Entry < ApplicationRecord
Message < Entry
Comment < Entry
如何显示包含 Message
和 Comment
记录的提要,这些记录可以轻松分页?好吧,你做不到!消息由消息表支持,评论由评论表支持。您无法同时从两个表中提取数据并使用一致的 OFFSET/LIMIT 方案。
您可以使用单表继承来解决分页问题,但现在您被迫使用一个包含所有子类所有属性的巨型表。无论差异有多大。如果 Message 有主题,但评论没有,那么现在评论也有了!因此,当子类及其属性之间的差异很小的时候,STI 最有效。
但还有第三种方法:委托类型。使用这种方法,“超类”是一个具体类,它由自己的表表示,所有在所有“子类”之间共享的超类属性都存储在该表中。然后,每个子类都有自己的独立表,用于存储特定于其实现的附加属性。这类似于 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
,多态性将为您提供答案。
嵌套属性
在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) 链接
将此定义为一个类,该类将为传递的role
委托其类型到types
中的类引用。这将创建一个指向该role
的多态belongs_to
关系,并且它将添加所有委托类型便利方法
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
来源:显示 | 在 GitHub 上
# 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