跳至内容 跳至搜索

委托类型

层次结构可以以多种方式映射到关系数据库表。例如,Active Record 提供了纯粹的抽象类,其中超类不持久化任何属性,以及单表继承,其中所有层次结构级别上的所有属性都表示在一个表中。两者都有其用途,但都不是没有缺点的。

纯粹抽象类的问题在于,所有具体子类都必须在其自己的表中持久化所有共享属性(也称为类表继承)。这使得跨层次结构进行查询变得困难。例如,假设您有以下层次结构

Entry < ApplicationRecord
Message < Entry
Comment < Entry

如何显示包含 MessageComment 记录的提要,这些记录可以轻松分页?好吧,你做不到!消息由消息表支持,评论由评论表支持。您无法同时从两个表中提取数据并使用一致的 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

如您所见,MessageComment 都不是独立存在的。两个类的关键元数据都驻留在 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

这使您可以为诸如ForwardsControllerRedeliverableController之类的东西创建控制器,它们都作用于条目,从而为消息和评论提供共享功能。

创建新记录

您可以通过同时创建委托人和被委托人来创建一个使用委托类型的新记录,如下所示

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'
方法
D

实例公共方法

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