Ruby on Rails 中的 Delegated Types(委托类型)

Ruby on Rails 中的 Delegated Types(委托类型)

什么是 Delegated Types?

在 Rails 中,delegated types 是一种处理多态关联的高级方式,适用于需要一个父类(superclass)管理多个子类(subclasses)的情况。它的核心思想是:

  • 有一个父类模型(比如 Entry),用来存储所有子类共享的属性。
  • 每个子类模型(比如 MessageComment)继承这个父类,并拥有自己独立的表,存储子类特有的属性。
  • 通过 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)
  1. 生成子类模型 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)
  1. 初步模型代码

运行完生成器后,模型文件如下:

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 的。

  1. 在 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 被删除时,关联的子类记录也会被删除。

  1. 定义 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 时间。

  1. 将模块应用到子类

修改 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 了。我们来试试创建和查询对象。

  1. 创建对象

在 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!"

  1. 查询对象
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)。
相关推荐
_UMR_31 分钟前
springboot集成Jasypt实现配置文件启动时自动解密-ENC
java·spring boot·后端
程序员小假37 分钟前
我们来说说 Cookie、Session、Token、JWT
java·后端
短剑重铸之日1 小时前
《SpringBoot4.0初识》第一篇:前瞻与思想
java·开发语言·后端·spring·springboot4.0
it_czz1 小时前
LangSmith vs LangFlow vs LangGraph Studio 可视化配置方案对比
后端
蓝色王者1 小时前
springboot 2.6.13 整合flowable6.8.1
java·spring boot·后端
花哥码天下2 小时前
apifox登录后设置token到环境变量
java·后端
hashiqimiya3 小时前
springboot事务触发滚动与不滚蛋
java·spring boot·后端
TeamDev3 小时前
基于 Angular UI 的 C# 桌面应用
前端·后端·angular.js
PPPHUANG3 小时前
一次 CompletableFuture 误用,如何耗尽 IO 线程池并拖垮整个系统
java·后端·代码规范