ActiveRecord - 进阶功能

Most of you are familiar with the virtues of a programmer. There are three, of course: laziness, impatience, and hubris. - Larry Wall

本章介绍其他ActiveRecord的常用进阶功能。

单一表格继承STI(Single-table inheritance)

如何将物件导向中的继承概念,对应到关联式数据库的设计,是个大哉问。Rails内建了其中最简单的一个解法,只用一个资料表储存继承体系中的物件,搭配一个type字段用来指名这笔资料的类别名称。

要开启STI功能,依照惯例只要有一个字段叫做type,型态字串即可。假设以下的contacts 资料表有字段叫做type,那么这三个Models实际上就会共享contacts一个资料表,当然,还有这两个子类别也都继承到父类别的validates_presence_of :name

  1. class Contact < ApplicationRecord
  2. validates_presence_of :name
  3. end
  4. class Company < Contact
  5. end
  6. class Person < Contact
  7. end

让我们进入rails console实验看看,Rails会根据你使用的类别,自动去设定type字段:

  1. contact = Person.create( :name => "ihower")
  2. contact.type # "Person"
  3. contact.id # 1
  4. contact = Company.create( :name => "ALPHA Camp" )
  5. contact.id # 2
  6. contact.type # "Company"

很遗憾,也因为这个惯例的关系,你不能将type这么名字挪做它用。

STI最大的问题在于字段的浪费,如果继承体系中交集的字段不多,那么使用STI就会非常的浪费空间。如果有较多的不共享的字段,笔者会建议不要使用这个功能,让个别的类别有自己的资料表。要关闭STI,请父类别加上self.abstract_class = true

  1. class Contact < ApplicationRecord
  2. self.abstract_class = true
  3. validates_presence_of :name
  4. end
  5. class Company < Contact
  6. end
  7. class Person < Contact
  8. end

这样CompanyPerson就需要有自己的Migrations建立companiespeople资料表了。

除了STI之外,在Patterns of Enterprise Application Architecture一书中也有介绍其他数据库处理OO继承的设计模式,包括Class Table InheritanceConcrete Table InheritanceInheritance Mappers

交易Transactions

Transaction(交易)保证所有资料的操作都只有在成功的情况下才会写入到数据库,最着名的例子也就是银行的帐户交易,只有在帐户提领金额及存入帐户这两个动作都成功的情况下才会将这笔操作写入数据库,否则在其中一个动作因为某些原因失败的话就会放弃所有已做的操作将资料回复到交易前的状态。在Rails中使用交易的方式像这样:

  1. ActiveRecord::Base.transaction do
  2. david.withdrawal(100)
  3. mary.deposit(100)
  4. end

你可以在一个交易中包含不同Active Record的类别或物件,这是因为交易是以数据库连线为范围,而不是个别Model

  1. User.transaction do
  2. User.create!(:name => 'ihower')
  3. Feed.create!
  4. end

注意到这里我们要使用create!而不是create,这是因为前者验证失败才会丢出例外,好让整个交易失败。同理,在交易里做更新应该使用update!而不是update

单一Modelsavedestroy方法已经帮你使用transaction包起来了,当资料验证失败或其中的回呼发生例外时,Rails就会触发rollback。所以下述的交易区块是多馀的不需要写:

  1. User.transaction do # 这是多馀的
  2. User.create!(:name => 'ihower')
  3. end

另外,由于资料的更新要在交易完成后才能被读取到,所以如果你在after_save回呼里让外部服务存取(例如呼叫全文搜寻引擎做索引),很可能因为交易尚未完成,会读取不到更新。这时候必须改用after_commit这个回呼,才能确保读取到交易完成后的资料。

Dirty objects

Dirty Objects功能可以追踪Model的属性是否有改变:

  1. person = Person.find_by_name('Uncle Bob')
  2. person.changed? # => false 没有改变任何值
  3. # 让我们来改一些值
  4. person.name = 'Bob'
  5. person.changed? # => true 有改变
  6. person.name_changed? # => true 这个属性有改变
  7. person.name_was # => 'Uncle Bob' 改变之前的值
  8. person.name_change # => ['Uncle Bob', 'Bob']
  9. person.name = 'Bill'
  10. person.name_change # => ['Uncle Bob', 'Bill']
  11. # 储存进数据库
  12. person.save
  13. person.changed? # => false
  14. person.name_changed? # => false
  15. # 看看哪些属性改变了
  16. person.name = 'Bob'
  17. person.changed # => ['name']
  18. person.changes # => { 'name' => ['Bill', 'Bob'] }

注意到Model资料一旦储存进数据库,追踪记录就重算消失了。

什么时候会用到这个功能呢?通常是在储存进数据库前的回呼、验证或Observer中,你想根据修改了什么来做些动作,这时候Dirty Objects功能就派上用场了。

序列化Serialize

序列化(Serialize)通常指的是将一个物件转换成一个可被数据库储存及传输的纯文字形态,反之将这笔资料从数据库读出后转回物件的动作我们就称之为反序列(Deserialize),Rails提供了serialize让你指定需要序列化资料的字段,任何物件在存入数据库时就会自动序列化成YAML格式,而当从数据库取出时就会自动帮你反序列成原先的物件。这个字段通常用text型态,有比较大的空间可以储存资料,然后将一个Hash物件序列化之后存进去。

常用的情境例如杂七杂八的使用者settings

  1. class User < ApplicationRecord
  2. serialize :settings
  3. end
  4. > user = User.create(:settings => { "sex" => "male", "url" => "foo" })
  5. > User.find(user.id).settings # => { "sex" => "male", "url" => "foo" }

或是一些不需要数据库索引和正规化的一整包资料,例如KML轨迹资料等等。

虽然序列化很方便可以让你储存任意的物件,但是缺点是序列化资料就失去了透过数据库查询索引的功效,你无法在SQLwhere条件中指定序列化后的资料。

Store

Store又在包裹了上一节的序列化功能,是个简单又实用的功能,让你可以将某个字段指定储存为Hash值。举例来说,上一节的settings也可以改用store来设定:

  1. class User < ApplicationRecord
  2. store :settings, :accessors => [:sex, :url]
  3. end

特别的是其中accessors用来设定可以直接存取的属性,这样就可以像平常一样那样操作sexurl这两个属性,让我们进console实验看看:

  1. > user = User.new(:sex => "male", :url => "http://example.com")
  2. > user.sex
  3. => "male"
  4. > user.url
  5. => "http://example.com"
  6. > user.settings
  7. => {:sex => "male", :url => "http://example.com"}

因为store就像使用hash一样,你也可以直接操作它,加入新的资料:

  1. > user.settings[:food] = "pizza"
  2. > user.settings
  3. => {:sex => "male", :url => "http://example.com", :food => "pizza"}

更多线上资源