Active Model の基礎

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

このガイドの内容:

  • Active Recordモデルの振る舞い
  • コールバックやバリデーションのしくみ
  • シリアライザのしくみ
  • Active ModelとRails国際化(i18n)フレームワークの統合方法

1 はじめに

Active Modelは多くのモジュールを含むライブラリであり、それらのモジュールはRailsのAction Packライブラリとやりとりする必要のあるフレームワークで使用されます。Active Modelは、クラスで使用する既知の一連のインターフェイスを提供します。そのうちのいくつかについて以下で説明します。

1.1 API

ActiveModel::API は、クラスをAction PackやAction Viewと連携させる機能をクラスに追加します。

class EmailContact
  include ActiveModel::API
  attr_accessor :name, :email, :message
  validates :name, :email, :message, presence: true
  def deliver
    if valid?
      # deliver email
    end
  end
end

ActiveModel::APIincludeすると、以下のような機能が使えるようになります。

  • モデル名のイントロスペクション
  • 変換
  • 翻訳(i18n)
  • バリデーション

また、Active Recordオブジェクトと同様に、属性のハッシュを持つオブジェクトを初期化する機能も使えます。

irb> email_contact = EmailContact.new(name: 'David', email: 'david@example.com', message: 'Hello World')
irb> email_contact.name
=> "David"
irb> email_contact.email
=> "david@example.com"
irb> email_contact.valid?
=> true
irb> email_contact.persisted?
=> false

ActiveModel::APIincludeしたクラスは、Active Recordオブジェクトと同様に、form_withrenderなどのAction Viewヘルパーでも利用できます。

1.2 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
irb> person = Person.new
irb> person.age = 110
irb> person.age_highest?
=> true
irb> person.reset_age
=> 0
irb> person.age_highest?
=> false

1.3 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.4 Conversionモジュール

クラスでpersisted?メソッドとidメソッドが定義されていれば、このActiveModel::ConversionモジュールをインクルードしてRailsの変換メソッドをそのクラスのオブジェクトに対して呼び出すことができます。

class Person
  include ActiveModel::Conversion

  def persisted?
    false
  end

  def id
    nil
  end
end
irb> person = Person.new
irb> person.to_model == person
=> true
irb> person.to_key
=> nil
irb> person.to_param
=> nil

1.5 Dirtyモジュール

あるオブジェクトが数度にわたって変更され、保存されていない状態は、「ダーティな(dirty:汚れた)」状態です。ActiveModel::Dirtyモジュールを使うと、オブジェクトで変更が生じたかどうかを検出できます。属性名に基づいたアクセサメソッドも使えます。first_name属性とlast_nameを持つPersonというクラスを例に考えてみましょう。

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.5.1 変更されたすべての属性のリストをオブジェクトから直接取得する
irb> person = Person.new
irb> person.changed?
=> false

irb> person.first_name = "First Name"
irb> person.first_name
=> "First Name"

# 属性が1つ以上変更されている場合にtrueを返す
irb> person.changed?
=> true

# 保存前に変更された属性のリストを返す
irb> person.changed
=> ["first_name"]

# 元の値から変更された属性のハッシュを返す
irb> person.changed_attributes
=> {"first_name"=>nil}

# 変更のハッシュを返す (ハッシュのキーは属性名、ハッシュの値はフィールドの新旧の値の配列)
irb> person.changes
=> {"first_name"=>[nil, "First Name"]}
1.5.2 属性名に基づいたアクセサメソッド

特定の属性が変更されたかどうかを検出します。

irb> person.first_name
=> "First Name"

# attr_name_changed?
irb> person.first_name_changed?
=> true

属性の直前の値を返します。

# attr_name_was accessor
irb> person.first_name_was
=> nil

変更された属性の、直前の値と現在の値を両方返します。変更があった場合は配列を返し、変更がなかった場合はnilを返します。

# attr_name_change
irb> person.first_name_change
=> [nil, "First Name"]
irb> person.last_name_change
=> nil

1.6 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
irb> person = Person.new
irb> person.token = "2b1f325"
irb> person.valid?
=> false
irb> person.name = 'vishnu'
irb> person.email = 'me'
irb> person.valid?
=> false
irb> person.email = 'me@vishnuatrai.com'
irb> person.valid?
=> true
irb> person.token = nil
irb> person.valid?
ActiveModel::StrictValidationFailed

1.7 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.8 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?
      # メールを配信
    end
  end
end

ActiveModel::Modelincludeすると、ActiveModel::APIのすべての機能を利用できるようになります。

1.9 シリアライズ

ActiveModel::Serializationは、オブジェクトに基本的なシリアライズ機能を提供します。シリアライズの対象となる属性を含む属性ハッシュを1つ宣言する必要があります。属性は文字列でなければならず、シンボルは使えません。

class Person
  include ActiveModel::Serialization

  attr_accessor :name

  def attributes
    { 'name' => nil }
  end
end

上のようにすることで、serializable_hashを使ってオブジェクトのシリアライズ化ハッシュにアクセスできるようになります。

irb> person = Person.new
irb> person.serializable_hash
=> {"name"=>nil}
irb> person.name = "Bob"
irb> person.serializable_hash
=> {"name"=>"Bob"}
1.9.1 ActiveModel::Serializersモジュール

Active Modelは、JSONシリアライズ/デシリアライズ用のActiveModel::Serializers::JSONモジュールも提供しています。このモジュールは前述のActiveModel::Serializationモジュールを自動でincludeします。

1.9.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メソッドは、モデルを表現するハッシュ形式を提供します。

irb> person = Person.new
irb> person.as_json
=> {"name"=>nil}
irb> person.name = "Bob"
irb> 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で属性を設定できるようになります。

irb> json = { name: 'Bob' }.to_json
irb> person = Person.new
irb> person.from_json(json)
=> #<Person:0x00000100c773f0 @name="Bob">
irb> person.name
=> "Bob"

1.10 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.11 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.12 SecurePasswordモジュール

ActiveModel::SecurePasswordは、任意のパスワードを暗号化して安全に保存する手段を提供します。このモジュールをincludeすると、バリデーション機能を備えたpasswordアクセサを定義するhas_secure_passwordクラスメソッドがデフォルトで提供されます。

1.12.1 必要条件

ActiveModel::SecurePasswordモジュールはbcrypt gemに依存しているので、ActiveModel::SecurePasswordを正しく使うにはこのgemをGemfileに含める必要があります。モジュールが機能するには、モデルにpassword_digestという名前のアクセサがなくてはなりません。has_secure_passwordpasswordアクセサに以下のバリデーションを追加します。

  1. パスワードが存在すること
  2. パスワードが(XXX_confirmationで渡された)パスワード確認入力と等しいこと
  3. パスワードの最大長が72文字であること(ActiveModel::SecurePasswordが依存しているbcryptによる要件であり、暗号化前にこの文字数まで切り詰められます)
1.12.2 例
class Person
  include ActiveModel::SecurePassword
  has_secure_password
  has_secure_password :recovery_password, validations: false

  attr_accessor :password_digest, :recovery_password_digest
end
irb> person = Person.new

# パスワードが空の場合
irb> person.valid?
=> false

# パスワード確認入力がパスワードと一致しない場合
irb> person.password = 'aditya'
irb> person.password_confirmation = 'nomatch'
irb> person.valid?
=> false

# パスワードが72文字を超えた場合
irb> person.password = person.password_confirmation = 'a' * 100
irb> person.valid?
=> false

# パスワードだけがありパスワード確認入力がない場合
irb> person.password = 'aditya'
irb> person.valid?
=> true

# すべてのバリデーションをパスした場合
irb> person.password = person.password_confirmation = 'aditya'
irb> person.valid?
=> true

irb> person.recovery_password = "42password"

irb> person.authenticate('aditya')
=> #<Person> # == person
irb> person.authenticate('notright')
=> false
irb> person.authenticate_password('aditya')
=> #<Person> # == person
irb> person.authenticate_password('notright')
=> false

irb> person.authenticate_recovery_password('42password')
=> #<Person> # == person
irb> person.authenticate_recovery_password('notright')
=> false

irb> person.password_digest
=> "$2a$04$gF8RfZdoXHvyTjHhiU4ZsO.kQqV9oonYZu31PRE4hLQn3xM2qkpIy"
irb> person.recovery_password_digest
=> "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加