Ruby on Rails 中的 Delegated Types(委托类型)
什么是 Delegated Types?
在 Rails 中,delegated types 是一种处理多态关联的高级方式,适用于需要一个父类(superclass)管理多个子类(subclasses)的情况。它的核心思想是:
- 有一个父类模型(比如
Entry
),用来存储所有子类共享的属性。 - 每个子类模型(比如
Message
和Comment
)继承这个父类,并拥有自己独立的表,存储子类特有的属性。 - 通过
delegated_type
方法,父类可以动态地指向不同的子类对象。
这种方式比传统的单一表继承(STI)更灵活,因为它避免了将所有子类的属性挤在一个表中,导致不必要的字段冗余。
举个生活中的例子
想象一个博客系统:
- 有两种内容类型:消息(
Message
) 和 评论(Comment
)。 - 它们都属于"条目(
Entry
)",共享一些属性(如创建时间),但也有各自独特的属性(比如消息有标题,评论有内容)。 - 我们希望用一个统一的
Entry
来管理它们,但又不想混淆它们的特有数据。
第一步:设计数据模型
我们需要一个父类和多个子类,并为它们生成对应的数据库表。
1. 生成父类模型 Entry
Entry
是所有条目的超类,负责存储共享属性,同时记录它关联的子类类型和 ID。
运行以下命令生成 Entry
模型:
bash
bin/rails generate model entry entryable_type:string entryable_id:integer
生成的表结构如下:
entries
- id (integer)
- entryable_type (string)
- entryable_id (integer)
- created_at (datetime)
- updated_at (datetime)
- 生成子类模型 Message 和 Comment
生成 Message 模型:
bash
bin/rails generate model message subject:string body:string
表结构:
messages
- id (integer)
- subject (string)
- body (string)
- created_at (datetime)
- updated_at (datetime)
生成 Comment 模型:
bash
bin/rails generate model comment content:string
表结构:
comments
- id (integer)
- content (string)
- created_at (datetime)
- updated_at (datetime)
- 初步模型代码
运行完生成器后,模型文件如下:
ruby
# app/models/entry.rb
class Entry < ApplicationRecord
end
ruby
# app/models/entry.rb
class Message < ApplicationRecord
end
ruby
# app/models/entry.rb
class Comment < ApplicationRecord
end
第二步:设置 Delegated Types
现在,我们需要告诉 Rails,Entry 是如何管理 Message 和 Comment 的。
- 在 Entry 中声明 delegated_type
在 Entry 模型中添加以下代码:
ruby
class Entry < ApplicationRecord
delegated_type :entryable, types: %w[Message Comment], dependent: :destroy
end
- delegated_type:Rails 提供的方法,用于设置委托类型。
- :entryable:指定多态关联的名称(对应 entryable_type 和 entryable_id 字段)。
- types: %w[Message Comment]:列出所有可能的子类。
- dependent: :destroy:当 Entry 被删除时,关联的子类记录也会被删除。
- 定义 Entryable 模块
子类需要知道它们属于某个 Entry,我们通过一个模块来实现这一点:
ruby
# app/models/concerns/entryable.rb
module Entryable
extend ActiveSupport::Concern
included do
has_one :entry, as: :entryable, touch: true
end
end
- has_one :entry, as: :entryable:表示子类有一个 Entry,并通过多态关联连接。
- touch: true:当子类更新时,自动更新 Entry 的 updated_at 时间。
- 将模块应用到子类
修改 Message 和 Comment 模型:
ruby
# app/models/message.rb
class Message < ApplicationRecord
include Entryable
end
ruby
# app/models/comment.rb
class Comment < ApplicationRecord
include Entryable
end
第三步:测试基本功能
现在,Entry 可以管理 Message 和 Comment 了。我们来试试创建和查询对象。
- 创建对象
在 Rails 控制台中运行:
ruby
# 创建一个 Message 类型的 Entry
Entry.create!(entryable: Message.new(subject: "Hello", body: "World"))
# 创建一个 Comment 类型的 Entry
Entry.create!(entryable: Comment.new(content: "Nice post!"))
数据库会记录:
entries:
- id: 1, entryable_type: "Message", entryable_id: 1
- id: 2, entryable_type: "Comment", entryable_id: 1
messages:
- id: 1, subject: "Hello", body: "World"
comments:
- id: 1, content: "Nice post!"
- 查询对象
ruby
# 获取所有 Entry
Entry.all
# => [#<Entry id: 1, entryable_type: "Message", entryable_id: 1>, ...]
# 获取第一个 Entry 的子类对象
entry = Entry.first
entry.entryable
# => #<Message id: 1, subject: "Hello", body: "World">
结论
通过 delegated types,我们成功地:
- 让 Entry 统一管理 Message 和 Comment,但仍然保持子类的独立性。
- 避免了 STI 可能导致的字段冗余问题。
- 让 Entry 动态地指向不同的子类,提高了灵活性。
这种模式适用于任何需要共享父类逻辑但仍希望保持子类表独立的场景,比如:
- 订单系统(Order 可以关联 PurchaseOrder 或 RefundOrder)。
- 通知系统(Notification 可以关联 EmailNotification 或 SMSNotification)。
- 活动系统(Event 可以关联 Webinar 或 Workshop)。