Active Model の基礎

このガイドでは、モデルクラスを使用して作業を開始するのに必要なことをすべて解説します。Action Packヘルパーは、Active Modelのおかげで非Active Recordモデルとやりとりすることができます。Active Modelを使用することで、カスタムのORM (オブジェクトリレーショナルマッピング) を作成してRailsフレームワークの外で使用することもできます。

このガイドの内容:

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::Modelincludeすると、以下のような機能を使えるようになります。

  • モデル名の調査
  • 変換
  • 翻訳
  • バリデーション

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::Modelincludeするクラスでは、Active Recordの場合と同様にform_forrenderなどの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_passwordpasswordアクセサに以下のバリデーションを追加します。

  1. パスワードが存在すること
  2. パスワードが(password_confirmationで渡された)パスワード確認入力と等しいこと
  3. パスワードの最大長が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