v7.1.3.2
更多資訊請至 rubyonrails.org: 更多 Ruby on Rails

Active Model 基礎

本指南應提供您開始使用模型類別所需的一切。Active Model 允許 Action Pack 輔助程式與純 Ruby 物件互動。Active Model 也協助建立自訂 ORM,供 Rails 架構外部使用。

閱讀本指南後,您將了解

1 什麼是 Active Model?

Active Model 是包含各種模組的函式庫,用於開發需要 Active Record 上某些功能的類別。以下是對其中一些模組的說明。

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::API 時,您會取得下列一些功能

  • 模型名稱內省
  • 轉換
  • 翻譯
  • 驗證

它也讓您可以使用屬性雜湊來初始化物件,就像任何 Active Record 物件一樣。

irb> email_contact = EmailContact.new(name: 'David', email: '[email protected]', message: 'Hello World')
irb> email_contact.name
=> "David"
irb> email_contact.email
=> "[email protected]"
irb> email_contact.valid?
=> true
irb> email_contact.persisted?
=> false

任何包含 ActiveModel::API 的類別都可以與 form_withrender 和任何其他 Action View 輔助方法一起使用,就像 Active Record 物件一樣。

1.2 屬性方法

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 回呼

ActiveModel::Callbacks 提供 Active Record 風格的回呼。這提供了定義在適當時機執行的回呼的能力。在定義回呼後,您可以使用自訂方法將它們包裝在 before、after 和 around 中。

class Person
  extend ActiveModel::Callbacks

  define_model_callbacks :update

  before_update :reset_me

  def update
    run_callbacks(:update) do
      # This method is called when update is called on an object.
    end
  end

  def reset_me
    # This method is called when update is called on an object as a before_update callback is defined.
  end
end

1.4 轉換

如果類別定義了 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_namelast_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
    # do save work...
    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"

# Returns true if any of the attributes have unsaved changes.
irb> person.changed?
=> true

# Returns a list of attributes that have changed before saving.
irb> person.changed
=> ["first_name"]

# Returns a Hash of the attributes that have changed with their original values.
irb> person.changed_attributes
=> {"first_name"=>nil}

# Returns a Hash of changes, with the attribute names as the keys, and the values as an array of the old and new values for that field.
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 驗證

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 = '[email protected]'
irb> person.valid?
=> true
irb> person.token = nil
irb> person.valid?
ActiveModel::StrictValidationFailed

1.7 命名

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 模型

ActiveModel::Model 允許實作類似於 ActiveRecord::Base 的模型。

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 時,您會取得 ActiveModel::API 中的所有功能。

1.9 序列化

ActiveModel::Serialization 為您的物件提供基本序列化。您需要宣告一個包含您要序列化的屬性的屬性雜湊。屬性必須是字串,而不是符號。

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 也提供 ActiveModel::Serializers::JSON 模組,用於 JSON 序列化/反序列化。此模組會自動包含先前討論過的 ActiveModel::Serialization 模組。

1.9.1.1 ActiveModel::Serializers::JSON

若要使用 ActiveModel::Serializers::JSON,您只需要將包含的模組從 ActiveModel::Serialization 變更為 ActiveModel::Serializers::JSON

class Person
  include ActiveModel::Serializers::JSON

  attr_accessor :name

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

as_json 方法類似於 serializable_hash,提供表示模型的雜湊。

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 翻譯

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 Testes de Lint

ActiveModel::Lint::Tests permite que você teste se um objeto está em conformidade com a API do Active Model.

  • 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
    
$ bin/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

Um objeto não é obrigado a implementar todas as APIs para funcionar com o Action Pack. Este módulo tem o objetivo apenas de orientar caso você queira todos os recursos prontos para uso.

1.12 SecurePassword

ActiveModel::SecurePassword fornece uma forma de armazenar com segurança qualquer senha em um formato criptografado. Quando você inclui este módulo, um método de classe has_secure_password é fornecido, que define um acessador password com certas validações nele por padrão.

1.12.1 Requisitos

ActiveModel::SecurePassword depende de bcrypt, então inclua esta gem no seu Gemfile para usar ActiveModel::SecurePassword corretamente. Para que isso funcione, o modelo deve ter um acessador chamado XXX_digest. Onde XXX é o nome do atributo da sua senha desejada. As seguintes validações são adicionadas automaticamente

  1. A senha deve estar presente.
  2. A senha deve ser igual à sua confirmação (fornecida XXX_confirmation é passada junto).
  3. O comprimento máximo de uma senha é de 72 bytes (obrigatório como bcrypt, do qual ActiveModel::SecurePassword depende, trunca a string para este tamanho antes de criptografá-la).

1.12.2 Exemplos

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

# When password is blank.
irb> person.valid?
=> false

# When the confirmation doesn't match the password.
irb> person.password = 'aditya'
irb> person.password_confirmation = 'nomatch'
irb> person.valid?
=> false

# When the length of password exceeds 72.
irb> person.password = person.password_confirmation = 'a' * 100
irb> person.valid?
=> false

# When only password is supplied with no password_confirmation.
irb> person.password = 'aditya'
irb> person.valid?
=> true

# When all validations are passed.
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"

Feedback

Você é encorajado a ajudar a melhorar a qualidade deste guia.

Contribua se você vir erros de digitação ou erros de fato. Para começar, você pode ler nossa seção de contribuições de documentação.

Você também pode encontrar conteúdo incompleto ou que não esteja atualizado. Adicione qualquer documentação ausente para o principal. Verifique primeiro Edge Guides para verificar se os problemas já foram corrigidos ou não no branch principal. Verifique as Diretrizes dos Guias do Ruby on Rails para estilo e convenções.

如果您發現需要修復但無法自行修補的任何原因,請 開啟問題

最後但並非最不重要的一點,任何有關 Ruby on Rails 文件的討論都非常歡迎在 官方 Ruby on Rails 論壇 中進行。