跳至内容 跳至搜索

委托类型

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

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

Entry < ApplicationRecord
Message < Entry
Comment < Entry

如何显示包含 MessageComment 记录的提要,这些记录可以轻松分页?好吧,你做不到!消息由 messages 表支持,而评论由 comments 表支持。您无法同时从两个表中提取数据并使用一致的 OFFSET/LIMIT 方案。

您可以使用单表继承来解决分页问题,但是现在您被迫使用一个包含所有子类所有属性的巨型表。无论差异有多大。如果 Message 有主题,但评论没有,那么现在评论也有了!因此,当子类及其属性之间的差异很小时,STI 最有效。

但是还有第三种方法:委托类型。使用这种方法,“超类”是一个具体的类,它由自己的表表示,其中存储了所有子类之间共享的超类属性。然后,每个子类都有自己的单独表,用于存储特定于其实现的附加属性。这类似于 Django 中的多表继承,但不是实际继承,这种方法使用委托来形成层次结构并共享责任。

让我们看看使用委托类型的条目/消息/评论示例

# Schema: entries[ id, account_id, creator_id, entryable_type, entryable_id, created_at, updated_at ]
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, created_at, updated_at ]
class Message < ApplicationRecord
  include Entryable
end

# Schema: comments[ id, content, created_at, updated_at ]
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, account: Current.account

如果您需要更复杂的组合,或者需要执行依赖验证,则应构建一个工厂方法或类来处理复杂的需要。这可以像下面这样简单

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

Querying 跨记录

委托类型的一个后果是,跨多个类查询属性变得稍微棘手,但并非不可能。

最简单的方法是将“超类”连接到“子类”,并在适当的位置应用查询参数(即 #where

Comment.joins(:entry).where(comments: { content: 'Hello!' }, entry: { creator: Current.user } )

为了方便起见,在关注点上添加一个范围。现在,所有实现该关注点的类将自动包含该方法

# app/models/concerns/entryable.rb
scope :with_entry, ->(attrs) { joins(:entry).where(entry: attrs) }

现在,查询可以显着缩短

Comment.where(content: 'Hello!').with_entry(creator: Current.user)

添加进一步的委托

委托类型不应该仅仅回答底层类的名称是什么这个问题。事实上,这在大多数情况下是一种反模式。您构建此层次结构的原因是为了利用多态性。以下是一个简单的示例

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