ActiveRecord - 进阶功能

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

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

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

    让我们进入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"

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

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

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

    交易Transactions

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

    你可以在一个交易中包含不同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. end

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

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

    序列化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轨迹资料等等。

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

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

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

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

    1. > user.settings[:food] = "pizza"
    2. > user.settings

    更多线上资源