1 はじめに
Active Modelは多くのモジュールを含むライブラリであり、それらのモジュールはRailsのAction Packライブラリとやりとりする必要のあるフレームワークで使用されます。Active Modelは、クラスで使用する既知の一連のインターフェイスを提供します。そのうちのいくつかについて以下で説明します。
1.1 AttributeMethodsモジュール
ActiveModel::AttributeMethods
モジュールは、クラスのメソッドにカスタムのプレフィックスやサフィックスを追加できます。このモジュールを使用するには、プレフィックスまたはサフィックスを定義し、オブジェクト内にあるプレフィックス/サフィックスの追加対象となるメソッドを指定します。
class Person include ActiveModel::AttributeMethods attribute_method_prefix 'reset_' attribute_method_suffix '_highest?' define_attribute_methods 'age' attr_accessor :age private def reset_attribute(attribute) send("#{attribute}=", 0) end def attribute_highest?(attribute) send(attribute) > 100 end end person = Person.new person.age = 110 person.age_highest? # true person.reset_age # 0 person.age_highest? # false
1.2 Callbacksモジュール
ActiveModel::Callbacks
は、Active Recordスタイルのコールバックを提供します。これにより、必要なタイミングで実行されるコールバックを定義することができるようになります。コールバックの定義後、それらをカスタムメソッドの実行前(before)、実行後(after)、あるいは実行中(around: beforeとafterの両方)にラップすることができます。
class Person extend ActiveModel::Callbacks define_model_callbacks :update before_update :reset_me def update run_callbacks(:update) do # updateメソッドがオブジェクトに対して呼び出されるとこのメソッドが呼び出される end end def reset_me # このメソッドは、before_updateコールバックで定義されているとおり、updateメソッドがオブジェクトに対して呼び出される直前に呼び出される。 end end
1.3 Conversionモジュール
クラスでpersisted?
メソッドとid
メソッドが定義されていれば、このActiveModel::Conversion
モジュールをインクルードしてRailsの変換メソッドをそのクラスのオブジェクトに対して呼び出すことができます。
class Person include ActiveModel::Conversion def persisted? false end def id nil end end person = Person.new person.to_model == person # => true person.to_key # => nil person.to_param # => nil
1.4 Dirtyモジュール
あるオブジェクトが数度にわたって変更され、保存されていない状態は、「汚れた (dirty)」状態です。ActiveModel::Dirty
モジュールを使うと、オブジェクトで変更が生じたかどうかを検出できます。属性名に基づいたアクセサメソッドも使えます。first_name
属性とlast_name
を持つPersonというクラスを例に考えてみましょう。
require 'active_model' class Person include ActiveModel::Dirty define_attribute_methods :first_name, :last_name def first_name @first_name end def first_name=(value) first_name_will_change! @first_name = value end def last_name @last_name end def last_name=(value) last_name_will_change! @last_name = value end def save # 保存を実行 changes_applied end end
1.4.1 変更されたすべての属性のリストをオブジェクトから直接取得する
person = Person.new person.changed? # => false person.first_name = "First Name" person.first_name # => "First Name" # 属性が1つ以上変更されている場合にtrueを返す person.changed? # => true # 保存前に変更された属性のリストを返す person.changed # => ["first_name"] # 元の値から変更された属性のハッシュを返す person.changed_attributes # => {"first_name"=>nil} # 変更のハッシュを返す (ハッシュのキーは属性名、ハッシュの値はフィールドの新旧の値の配列 person.changes # => {"first_name"=>[nil, "First Name"]}
1.4.2 属性名に基づいたアクセサメソッド
特定の属性が変更されたかどうかを検出します。
# attr_name_changed? person.first_name # => "First Name" person.first_name_changed? # => true
その属性の直前の値を返します。
# attr_name_was accessor person.first_name_was # => nil
変更された属性の、直前の値と現在の値を両方返します。変更があった場合は配列を返し、変更がなかった場合はnilを返します。
# attr_name_change person.first_name_change # => [nil, "First Name"] person.last_name_change # => nil
1.5 Validations
モジュール
ActiveModel::Validations
モジュールを追加すると、クラスオブジェクトをActive Recordスタイルで検証できます。
class Person include ActiveModel::Validations attr_accessor :name, :email, :token validates :name, presence: true validates_format_of :email, with: /\A([^\s]+)((?:[-a-z0-9]\.)[a-z]{2,})\z/i validates! :token, presence: true end person = Person.new(token: "2b1f325") person.valid? # => false person.name = 'vishnu' person.email = 'me' person.valid? # => false person.email = 'me@vishnuatrai.com' person.valid? # => true person.token = nil person.valid? # => ActiveModel::StrictValidationFailedが発生する
1.6 Naming
モジュール
ActiveModel::Naming
は、命名やルーティングの管理を支援するクラスメソッドを多数追加します。このモジュールが定義するmodel_name
クラスメソッドは、ActiveSupport::Inflector
メソッドの一部を用いて多くのアクセサを定義します。
class Person extend ActiveModel::Naming end Person.model_name.name # => "Person" Person.model_name.singular # => "person" Person.model_name.plural # => "people" Person.model_name.element # => "person" Person.model_name.human # => "Person" Person.model_name.collection # => "people" Person.model_name.param_key # => "person" Person.model_name.i18n_key # => :person Person.model_name.route_key # => "people" Person.model_name.singular_route_key # => "person"
1.7 Model
モジュール
ActiveModel::Model
を追加すると、Action PackやAction Viewと連携する機能をすぐに使えるようになります。
class EmailContact include ActiveModel::Model attr_accessor :name, :email, :message validates :name, :email, :message, presence: true def deliver if valid? # deliver email end end end
ActiveModel::Model
をinclude
すると、以下のような機能を使えるようになります。
- モデル名の調査
- 変換
- 翻訳
- バリデーション
Active Recordの場合と同じような方法で、オブジェクトを属性のハッシュで初期化することもできるようになります。
email_contact = EmailContact.new(name: 'David', email: 'david@example.com', message: 'Hello World') email_contact.name # => 'David' email_contact.email # => 'david@example.com' email_contact.valid? # => true email_contact.persisted? # => false
ActiveModel::Model
をinclude
するクラスでは、Active Recordの場合と同様にform_for
やrender
などのAction Viewヘルパーメソッドを使えるようになりま。
1.8 シリアライズ
ActiveModel::Serialization
は、オブジェクトに基本的なシリアライズ機能を提供します。シリアライズの対象となる属性を含む属性ハッシュを1つ宣言する必要があります。属性は文字列でなければならず、シンボルは使えません。
class Person include ActiveModel::Serialization attr_accessor :name def attributes {'name' => nil} end end
上のようにすることで、serializable_hash
を使ってオブジェクトのシリアライズ化ハッシュにアクセスできるようになります。
person = Person.new person.serializable_hash # => {"name"=>nil} person.name = "Bob" person.serializable_hash # => {"name"=>"Bob"}
1.8.1 ActiveModel::Serializers
モジュール
Active Modelは、JSONシリアライズ/デシリアライズ用のActiveModel::Serializers::JSON
モジュールも提供しています。このモジュールは前述のActiveModel::Serialization
モジュールを自動でinclude
します。
1.8.1.1 ActiveModel::Serializers::JSON
include
するモジュールをActiveModel::Serialization
からActiveModel::Serializers::JSON
に変更するだけでActiveModel::Serializers::JSON
を使えるようになります。
class Person include ActiveModel::Serializers::JSON attr_accessor :name def attributes {'name' => nil} end end
serializable_hash
と似ているas_json
メソッドは、モデルを表現するハッシュ形式を提供します。
person = Person.new person.as_json # => {"name"=>nil} person.name = "Bob" person.as_json # => {"name"=>"Bob"}
JSON文字列を元にモデルの属性を定義することもできます。ただし、そのクラスにattributes=
メソッドを定義しておく必要があります。
class Person include ActiveModel::Serializers::JSON attr_accessor :name def attributes=(hash) hash.each do |key, value| send("#{key}=", value) end end def attributes {'name' => nil} end end
上のようにすることで、Person
のインスタンスを作成してfrom_json
で属性を設定できるようになります。
json = { name: 'Bob' }.to_json person = Person.new person.from_json(json) # => #<Person:0x00000100c773f0 @name="Bob"> person.name # => "Bob"
1.9 Translation
モジュール
ActiveModel::Translation
は、オブジェクトとRails国際化(i18n)フレームワーク間の統合機能を提供します。
class Person extend ActiveModel::Translation end
human_attribute_name
メソッドを使って属性名を人間にとって読みやすい形式に変換できます。人間が読むための形式は独自のロケールファイルで定義します。
- config/locales/app.pt-BR.yml
pt-BR: activemodel: attributes: person: name: 'Nome'
Person.human_attribute_name('name') # => "Nome"
1.10 Lintテスト
ActiveModel::Lint::Tests
を用いて、オブジェクトがActive Model APIに準拠しているかどうかをテストできます。
app/models/person.rb
class Person include ActiveModel::Model end
test/models/person_test.rb
require 'test_helper' class PersonTest < ActiveSupport::TestCase include ActiveModel::Lint::Tests setup do @model = Person.new end end
$ rails test Run options: --seed 14596 # Running: ...... Finished in 0.024899s, 240.9735 runs/s, 1204.8677 assertions/s. 6 runs, 30 assertions, 0 failures, 0 errors, 0 skips
オブジェクトがAction Packと協調するためにAPIをすべて実装することが要求されているわけではありません。このモジュールは、すぐに使える機能をすべて揃えておきたい場合のガイダンスを提供することを意図しているに過ぎません。
1.11 SecurePassword
モジュール
ActiveModel::SecurePassword
は、任意のパスワードを暗号化して安全に保存する手段を提供します。このモジュールをinclude
すると、バリデーション機能を備えたpassword
アクセサを定義するhas_secure_password
クラスメソッドが提供されます。
1.11.1 必要条件
ActiveModel::SecurePassword
モジュールはbcrypt
gemに依存しているので、ActiveModel::SecurePassword
を正しく使うにはこのgemをGemfile
に含める必要があります。モジュールが機能するには、モデルにpassword_digest
という名前のアクセサがなくてはなりません。has_secure_password
はpassword
アクセサに以下のバリデーションを追加します。
- パスワードが存在すること
- パスワードが(
password_confirmation
で渡された)パスワード確認入力と等しいこと - パスワードの最大長が72文字であること(
ActiveModel::SecurePassword
が依存しているbcrypt
による要求)
1.11.2 例
class Person include ActiveModel::SecurePassword has_secure_password attr_accessor :password_digest end person = Person.new # パスワードが空の場合 person.valid? # => false # パスワード確認入力がパスワードと一致しない場合 person.password = 'aditya' person.password_confirmation = 'nomatch' person.valid? # => false # パスワードが72文字を超えた場合 person.password = person.password_confirmation = 'a' * 100 person.valid? # => false # パスワードだけがありパスワード確認入力がない場合 person.password = 'aditya' person.valid? # => true # すべてのバリデーションをパスした場合 person.password = person.password_confirmation = 'aditya' person.valid? # => true