rails: 单表继承(STI)-Single Table Inheritance

在 Rails 中,单表继承(Single Table Inheritance,简称 STI)是一种允许不同类型的模型共享同一个数据库表的技术。这些不同类型的模型通过表中的一个特定列(通常称为 type)来区分。STI 的典型使用场景是当你有多个相关的模型,它们共享大部分的字段,但某些字段和行为又有所不同。通过 STI,可以避免为每个模型创建单独的数据库表,简化了数据库结构。

一个常见的使用场景是处理多种类型的用户或角色。例如,在一个系统中,你可能有三种不同类型的用户:

  • 普通用户(User)
  • 管理员(Admin)
  • 超级管理员(SuperAdmin)。
  • 这些用户有一些相同的属性(如名字、邮箱、密码等),但又有一些不同的功能或权限。

在 Rails 中,type 字段通常使用 string 类型,因为它需要存储不同子类的名称(如 UserAdminSuperAdmin 等)。不过,理论上,你可以使用其他数据类型来实现单表继承,但这并不是最佳实践。

实践下来

  • 如果使用基础模型,创建实例, type 为 nil 与 chatgpt 的结果不一致(ChatGpt给的是 User 的 type 还是为 User)

01 添加基础模型(User)-ParentModel/BasicModel ✅

首先,生成 User 模型,并为其添加基础字段,如 nameemailpassword_digest,以及 type 字段(用于存储 STI 类型信息)。

  • 注意: type: string 就是最佳实践
# 1. 基础模型
rails g model User type:string name:string email:string password_digest:string
# 2. 生成Admin
bin/rails generate model Admin --parent=User
# 3. 生成SuperAdmin
bin/rails generate model SuperAdmin --parent=User

02 生成的迁移文件

这个命令会生成一个数据库迁移文件,位于 db/migrate/ 目录下,类似于以下内容:

class CreateUsers < ActiveRecord::Migration[8.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email
      t.string :password_digest
      t.string :type

      t.timestamps
    end
  end
end

type 字段是单表继承(STI)的关键字段,用于区分继承自 User 的子类,如 AdminSuperAdmin

03 定义模型User/Admin/SuperAdmin - Child Models ✅

接下来,定义模型。在 app/models/ 目录下,Rails 会自动为你生成 user.rb 模型文件,你只需要手动创建 admin.rbsuper_admin.rb 模型文件,并继承 User 类。

  • User
  • Admin
  • SuperAdmin

User 模型 (app/models/user.rb)

class User < ApplicationRecord
  # 这里可以放置所有用户共有的逻辑
  has_secure_password
end

Admin 模型 (app/models/admin.rb)

class Admin < User
  # 管理员特定的逻辑
end

SuperAdmin 模型 (app/models/super_admin.rb)

class SuperAdmin < User
  # 超级管理员特定的逻辑
end

04 使用单表继承 ✅

你可以使用 User.createAdmin.createSuperAdmin.create 来创建不同类型的用户,type 字段会自动区分它们:

User.create(name: "Alice", email: "alice@example.com", password: "password") # 创建普通用户
Admin.create(name: "Bob", email: "bob@example.com", password: "password") # 创建管理员
SuperAdmin.create(name: "Charlie", email: "charlie@example.com", password: "password") # 创建超级管理员

注意

User 的实例通常不会出现在逻辑上需要区分子类的地方,以避免实例化一个没有特定角色的“裸”用户而带来的数据和逻辑不一致的问题。

05 优缺点

Single Table Inheritance (STI)当子类和它们的属性之间差别不大的时候,效果最好,但它在单个表中包含所有子类的所有属性。

这种方法的缺点是它会导致表膨胀,因为表将包含每个子类特有的属性,即使这些属性未被其他子类使用。这可以通过使用来解决Delegated Types

此外,如果您使用多态关联,其中一个模型可以通过类型和 ID 属于多个其他模型,则维护引用完整性可能会变得复杂,因为关联逻辑必须正确处理不同类型。

最后,如果您有特定数据完整性检查或验证(这些检查或验证在子类之间有所不同),则需要确保这些检查或验证能够由 Rails 或数据库正确处理,尤其是在设置外键约束时。

rubyonrails rails sti