Rails3: ActiveModel 實作

我在 Rails3 Beta 發佈: 重點導覽 中提到過 ActiveModel,今天讓我們更深入看看什麼時候會用到,以及怎麼使用它。

ActiveModel 定義了一組介面可以與 ActionPack helpers 銜接,也提供了許多現成的 Module 功能。任何 Class 只要實作了 ActiveModel 的介面,然後選配 include 你需要的 Module,就可以與 Rails3 helpers 銜接在一起了。

什麼時候會用到 ActiveModel 呢? 例如你想要換不同的 ORM 而不使用 ActiveRecord,或是你沒有 backend storage 只是想要拿來做其他動作(例如單純的聯絡表單用來寄信)。在 Building Web Service Clients with ActiveModel 這篇投影片中,Paul Dix 也將 ActiveModel 應用在 SOA 架構的 client-side model (就不要用 ActiveResource 吧,用 ActiveModel 配上 HTTP client library 自己寫一個比較合用)。

讓我們來看看 ActiveModel 定義的介面吧,你的 Model 得實作以下方法:

1. to_model, to_key, to_param, persisted?
2. model_name 以及 model_name.human, model_name.partial_path, model_name.singular, model_name.plural
3. valid?, errors 以及 errors[], errors.full_messages

這樣才能與 ActionPack helpers 接起來(這些 helpers 例如 link_to, form_for 等等)。所幸要預設實作這樣介面非常容易,除了 persisted? 之外,ActiveModel 都有提供現成的 Module 可以使用:


class YourModel
    extend ActiveModel::Naming
    include ActiveModel::Conversion
    include ActiveModel::Validations
     
    def persisted?
        false
    end
end

其中 persisted? 的實作得依照你的 backend storage 而定。如果你的 Model 沒有 backend storage,那麼就是 false。
而 ActiveModel::Validations 提供了你熟悉的各種 validation 方法,例如 validates_presence_of, validates_format_of 等等 (除了 validates_uniqueness_of 之外,原因不證自明)。

如果你不想要整套的 validation 框架,也可以不 include ActiveModel::Validations。你可以讓 valid? 恆真,然後使用 ActiveModel::Errors 定義 errors 方法即可滿足介面要求:


class YourModel
    extend ActiveModel::Naming
    include ActiveModel::Conversion

    def valid?() true end



    def errors
    
        @errors ||= ActiveModel::Errors.new(self)
  
    end

    def persisted?
        false
    end
end

以上這些介面 ActiveModel 也有提供 Lint Tests 可以測試,只要 include 它即可:


require 'test_helper'
class YourModelTest < ActiveSupport::TestCase
  include ActiveModel::Lint::Tests
end

以上就是 ActiveModel 介面的最低要求了,透過 ActiveModel 內建的 Naming, Conversion, Validations module 就可以輕易實作出來。ActiveModel 還有提供許多好用的 Module 來擴充,包括有:

就不一一詳述了,詳細的用法請看原始碼及文件說明。最後提一下 MassAssignment。

MassAssignmentSecurity

如果你想要實作 MassAssignment 功能,就是有 YourModel.new(params[:model]) 這樣的介面,你可以這樣實作:


class YourModel
    # ...
    def initialize(attributes = {})
        if attributes.present?
          attributes.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
        end
      end
end

但是提到 MassAssignment,就可能會有 MassAssignment Security 的需求,我們想要保護一些特定的屬性不能透過 Mass Assignment 來設定值。
ActiveModel 提供了 MassAssignmentSecurity 這個 Module 讓我們可以獲得 attr_protected 和 attr_accessible 方法來保護特定屬性。用法如下:


class YourModel
    # ...
    include ActiveModel::MassAssignmentSecurity
    
    def initialize(attributes = {})
        if attributes.present?
          sanitize_for_mass_assignment(attributes).each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
        end
    end
end

參考資料

Leave a Reply