STI 是 Single-table inheritance 的缩写,顾名思义,就是用一张数据表来表示 model 间的继承关系。

话不多说上代码:

class Employee < ActiveRecord::Base
end

class Manager < Employee
end

class Worker < Employee
end

请注意!在 Rails5 中默认新建的 model 是继承自 ApplicationRecord 类,我们要改成 ActiveRecord::Base 类,原因我先卖个关子,等到最后解释。

在这个例子中,我定义了一个 Employee 类,然后 Employee 类有两个子类分别是 Manager 和 Worker。这个业务场景很好理解,首先一个公司所有的员工都肯定是 employee,然后他们还分别是 manager 或者 worker(有的业务场景更复杂,一个 employee 可以同时既是 manager 也是 worker,词面上更准确的说应该用 supervisor,A 是 B 的 supervisor,C 是 A 的 supervisor,也就是说 A 同时又是 C 的 worker。不过这个模型不在本文的讨论范围)。

想象一下,如果没有 STI 技术,我们会如何处理这三个 model 在数据库中的映射呢?没错,为每个 model 建一张表。很显然,我们可以发现在这种业务场景中,这三个模型的字段是完全一样的,或者极度相似的:manager 和 worker 都是 employee,他们都应该有 id、name、department、position 等等一系列公司职员属性,只不过他们会有不一样的行为逻辑。所以,把这样三个模型分别用三张表存储数据就非常浪费了。

对于那些字段完全相同或者极度相似,但是行为和方法有差异的继承类,使用 STI 技术来节省数据库空间,同时使代码更清晰。

Rails 內建了一个非常简单的方法,只用一个数据表来存储继承关系,利用一个叫做 type 的默认字段来指明继承体系中的模型名称,这就是 Rails 中的 STI 实现。因此,type 也是 Rails 的约束保留字段,不能挪作他用,笔者刚开始用 Rails 的时候就碰到过这样的错误:尝试给一个字段命名为 type 结果却报错,原因就在这里。

当然,type 保留字段也并非不可配置,如果想将 type 挪作他用,或者设置自定义的继承字段名,可以用 self.inheritance_column = 'custom_column' 的方法重定义,详见 Rails API

同样来看代码,只要在 employees 表中建立一个 type 字段,就能开启 STI 功能,让 Employee、Manager、Worker 这三个 model 共用 employees 一张表了:

employee = Manager.create( :name => "Aaron")
employee.type           # "Manager"
employee.id             # 1
employee = Worker.create( :name => "Betty" )
employee.id             # 2
employee.type           # "Worker"

如果要关闭 STI 功能,只要在父类中加上 self.abstract_class = true 即可。 好了,接下来我要解答为什么在 Rails 中要用 STI 功能,类只能继承自 ActiveRecord::Base 而不可以是 Rails5 默认的 ApplicationRecord。我们直接来看 Rails5 中 ApplicationRecord 的定义:

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

看到代码相信我不用多说你也能明白了,Rails5 新增的 ApplicationRecord 类只是在 ActiveRecord::Base 上又包了一层,作用仅仅是关闭了 STI 功能。所以,Rails5 默认是为所有 model 关闭 STI 的,如果要使用 STI,只需把要继承的类改成 ActiveRecord::Base 就可以了。


参考资料: