更多内容请访问 rubyonrails.org:

Active Model 基础

本指南将提供您开始使用 Active Model 所需的一切。Active Model 提供了一种让 Action Pack 和 Action View 助手与普通 Ruby 对象交互的方式。它还有助于构建用于 Rails 框架之外的自定义 ORM。

阅读本指南后,您将了解

  • Active Model 是什么,以及它与 Active Record 的关系。
  • Active Model 中包含的不同模块。
  • 如何在您的类中使用 Active Model。

1. 什么是 Active Model?

要理解 Active Model,您需要对 Active Record 有所了解。Active Record 是一个 ORM(对象关系映射),它将需要持久存储数据的对象连接到关系数据库。但是,它具有在 ORM 之外也很有用的功能,其中一些功能包括验证、回调、翻译、创建自定义属性的能力等。

其中一些功能从 Active Record 中抽象出来,形成了 Active Model。Active Model 是一个库,包含各种模块,这些模块可用于需要类似模型功能但未绑定到数据库中任何表的普通 Ruby 对象。

总之,Active Record 提供了一个定义与数据库表对应的模型的接口,而 Active Model 提供了构建不需要数据库支持的类似模型的 Ruby 类的功能。Active Model 可以独立于 Active Record 使用。

其中一些模块将在下面解释。

1.1. API

ActiveModel::API 使类能够直接与 Action PackAction View 配合使用。

包含 ActiveModel::API 时,默认会包含其他模块,使您能够获得以下功能:

这是一个包含 ActiveModel::API 的类的示例以及如何使用它:

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
irb> email_contact = EmailContact.new(name: "David", email: "david@example.com", message: "Hello World")

irb> email_contact.name # Attribute Assignment
=> "David"

irb> email_contact.to_model == email_contact # Conversion
=> true

irb> email_contact.model_name.name # Naming
=> "EmailContact"

irb> EmailContact.human_attribute_name("name") # Translation if the locale is set
=> "Name"

irb> email_contact.valid? # Validations
=> true

irb> empty_contact = EmailContact.new
irb> empty_contact.valid?
=> false

任何包含 ActiveModel::API 的类都可以像 Active Record 对象一样与 form_withrender 以及任何其他 Action View 助手方法一起使用。

例如,form_with 可以用于为 EmailContact 对象创建表单,如下所示:

<%= form_with model: EmailContact.new do |form| %>
  <%= form.text_field :name %>
<% end %>

这会生成以下 HTML:

<form action="/email_contacts" method="post">
  <input type="text" name="email_contact[name]" id="email_contact_name">
</form>

render 可以用于使用对象渲染部分:

<%= render @email_contact %>

您可以在 Action View 表单助手布局和渲染 指南中分别了解更多关于如何将 form_withrenderActiveModel::API 兼容对象一起使用。

1.2. 模型

ActiveModel::Model 默认包含 ActiveModel::API 以与 Action Pack 和 Action View 交互,是实现类模型 Ruby 类的推荐方法。未来将对其进行扩展以添加更多功能。

class Person
  include ActiveModel::Model

  attr_accessor :name, :age
end
irb> person = Person.new(name: 'bob', age: '18')
irb> person.name # => "bob"
irb> person.age  # => "18"

1.3. 属性

ActiveModel::Attributes 允许您在普通 Ruby 对象上定义数据类型、设置默认值以及处理类型转换和序列化。这对于表单数据可能很有用,它将为常规对象上的日期和布尔值等提供类似 Active Record 的转换。

要使用 Attributes,请在您的模型类中包含该模块,并使用 attribute 宏定义您的属性。它接受名称、转换类型、默认值以及属性类型支持的任何其他选项。

class Person
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :date_of_birth, :date
  attribute :active, :boolean, default: true
end
irb> person = Person.new

irb> person.name = "Jane"
irb> person.name
=> "Jane"

# Casts the string to a date set by the attribute
irb> person.date_of_birth = "2020-01-01"
irb> person.date_of_birth
=> Wed, 01 Jan 2020
irb> person.date_of_birth.class
=> Date

# Uses the default value set by the attribute
irb> person.active
=> true

# Casts the integer to a boolean set by the attribute
irb> person.active = 0
irb> person.active
=> false

使用 ActiveModel::Attributes 时,可以使用下面描述的一些附加方法。

1.3.1. 方法: attribute_names

attribute_names 方法返回一个属性名称数组。

irb> Person.attribute_names
=> ["name", "date_of_birth", "active"]

1.3.2. 方法: attributes

attributes 方法返回所有属性的哈希,其中属性名称作为键,属性值作为值。

irb> person.attributes
=> {"name" => "Jane", "date_of_birth" => Wed, 01 Jan 2020, "active" => false}

1.4. 属性赋值

ActiveModel::AttributeAssignment 允许您通过传入属性哈希来设置对象的属性,其中键与属性名称匹配。当您希望一次设置多个属性时,这非常有用。

考虑以下类:

class Person
  include ActiveModel::AttributeAssignment

  attr_accessor :name, :date_of_birth, :active
end
irb> person = Person.new

# Set multiple attributes at once
irb> person.assign_attributes(name: "John", date_of_birth: "1998-01-01", active: false)

irb> person.name
=> "John"
irb> person.date_of_birth
=> Thu, 01 Jan 1998
irb> person.active
=> false

如果传入的哈希响应 permitted? 方法,并且此方法的返回值为 false,则会引发 ActiveModel::ForbiddenAttributesError 异常。

permitted? 用于 强参数 集成,您正在从请求中分配参数属性。

irb> person = Person.new

# Using strong parameters checks, build a hash of attributes similar to params from a request
irb> params = ActionController::Parameters.new(name: "John")
=> #<ActionController::Parameters {"name" => "John"} permitted: false>

irb> person.assign_attributes(params)
=> # Raises ActiveModel::ForbiddenAttributesError
irb> person.name
=> nil

# Permit the attributes we want to allow assignment
irb> permitted_params = params.permit(:name)
=> #<ActionController::Parameters {"name" => "John"} permitted: true>

irb> person.assign_attributes(permitted_params)
irb> person.name
=> "John"

1.4.1. 方法别名: attributes=

assign_attributes 方法有一个别名 attributes=

方法别名是一个执行与另一个方法相同操作的方法,但名称不同。别名是为了可读性和方便性而存在的。

以下示例演示了使用 attributes= 方法一次设置多个属性:

irb> person = Person.new

irb> person.attributes = { name: "John", date_of_birth: "1998-01-01", active: false }

irb> person.name
=> "John"
irb> person.date_of_birth
=> "1998-01-01"

assign_attributesattributes= 都是方法调用,并接受要赋值的属性哈希作为参数。在许多情况下,Ruby 允许省略方法调用中的括号 () 和哈希定义中的花括号 {}

attributes= 这样的“setter”方法通常不带 () 编写,即使包含它们也一样工作,并且它们要求哈希始终包含 {}person.attributes=({ name: "John" }) 是可以的,但 person.attributes = name: "John" 会导致 SyntaxError

其他方法调用(如 assign_attributes)可能包含或不包含哈希参数的括号 (){}。例如,assign_attributes name: "John"assign_attributes({ name: "John" }) 都是完全有效的 Ruby 代码,但是 assign_attributes { name: "John" } 不是,因为 Ruby 无法将该哈希参数与块区分开来,并将引发 SyntaxError

1.5. 属性方法

ActiveModel::AttributeMethods 提供了一种为模型属性动态定义方法的方式。此模块特别有助于简化属性访问和操作,并且可以为类的属性方法添加自定义前缀和后缀。您可以定义前缀和后缀以及对象上哪些方法将使用它们,如下所示:

  1. 在您的类中包含 ActiveModel::AttributeMethods
  2. 调用您要添加的每个方法,例如 attribute_method_suffixattribute_method_prefixattribute_method_affix
  3. 在其他方法之后调用 define_attribute_methods 以声明应添加前缀和后缀的属性。
  4. 定义您已声明的各种通用 _attribute 方法。这些方法中的参数 attribute 将被 define_attribute_methods 中传入的参数替换。在下面的示例中是 name

attribute_method_prefixattribute_method_suffix 用于定义将用于创建方法的前缀和后缀。attribute_method_affix 用于同时定义前缀和后缀。

class Person
  include ActiveModel::AttributeMethods

  attribute_method_affix prefix: "reset_", suffix: "_to_default!"
  attribute_method_prefix "first_", "last_"
  attribute_method_suffix "_short?"

  define_attribute_methods "name"

  attr_accessor :name

  private
    # Attribute method call for 'first_name'
    def first_attribute(attribute)
      public_send(attribute).split.first
    end

    # Attribute method call for 'last_name'
    def last_attribute(attribute)
      public_send(attribute).split.last
    end

    # Attribute method call for 'name_short?'
    def attribute_short?(attribute)
      public_send(attribute).length < 5
    end

    # Attribute method call 'reset_name_to_default!'
    def reset_attribute_to_default!(attribute)
      public_send("#{attribute}=", "Default Name")
    end
end
irb> person = Person.new
irb> person.name = "Jane Doe"

irb> person.first_name
=> "Jane"
irb> person.last_name
=> "Doe"

irb> person.name_short?
=> false

irb> person.reset_name_to_default!
=> "Default Name"

如果您调用未定义的方法,它将引发 NoMethodError 错误。

1.5.1. 方法: alias_attribute

ActiveModel::AttributeMethods 使用 alias_attribute 提供属性方法的别名。

下面的示例为 name 创建一个名为 full_name 的别名属性。它们返回相同的值,但别名 full_name 更好地反映了该属性包含名字和姓氏。

class Person
  include ActiveModel::AttributeMethods

  attribute_method_suffix "_short?"
  define_attribute_methods :name

  attr_accessor :name

  alias_attribute :full_name, :name

  private
    def attribute_short?(attribute)
      public_send(attribute).length < 5
    end
end
irb> person = Person.new
irb> person.name = "Joe Doe"
irb> person.name
=> "Joe Doe"

# `full_name` is the alias for `name`, and returns the same value
irb> person.full_name
=> "Joe Doe"
irb> person.name_short?
=> false

# `full_name_short?` is the alias for `name_short?`, and returns the same value
irb> person.full_name_short?
=> false

1.6. 回调

ActiveModel::Callbacks 为普通 Ruby 对象提供 Active Record 风格的回调。回调允许您挂接到模型生命周期事件,例如 before_updateafter_create,以及定义在模型生命周期中的特定点执行的自定义逻辑。

您可以按照以下步骤实现 ActiveModel::Callbacks

  1. 在您的类中扩展 ActiveModel::Callbacks
  2. 使用 define_model_callbacks 建立一个应与回调关联的方法列表。当您指定一个方法(例如 :update)时,它将自动包含 :update 事件的所有三个默认回调(beforearoundafter)。
  3. 在定义的方法内部,使用 run_callbacks,它将在触发特定事件时执行回调链。
  4. 在您的类中,您可以使用 before_updateafter_updatearound_update 方法,就像在 Active Record 模型中使用它们一样。
class Person
  extend ActiveModel::Callbacks

  define_model_callbacks :update

  before_update :reset_me
  after_update :finalize_me
  around_update :log_me

  # `define_model_callbacks` method containing `run_callbacks` which runs the callback(s) for the given event
  def update
    run_callbacks(:update) do
      puts "update method called"
    end
  end

  private
    # When update is called on an object, then this method is called by `before_update` callback
    def reset_me
      puts "reset_me method: called before the update method"
    end

    # When update is called on an object, then this method is called by `after_update` callback
    def finalize_me
      puts "finalize_me method: called after the update method"
    end

    # When update is called on an object, then this method is called by `around_update` callback
    def log_me
      puts "log_me method: called around the update method"
      yield
      puts "log_me method: block successfully called"
    end
end

上述类将产生以下结果,表明回调的调用顺序:

irb> person = Person.new
irb> person.update
reset_me method: called before the update method
log_me method: called around the update method
update method called
log_me method: block successfully called
finalize_me method: called after the update method
=> nil

根据上面的示例,在定义“around”回调时,请记住 yield 到块,否则它将不会执行。

传递给 define_model_callbacksmethod_name 不得以 !?= 结尾。此外,多次定义相同的回调将覆盖以前的回调定义。

1.6.1. 定义特定回调

您可以通过将 only 选项传递给 define_model_callbacks 方法来选择创建特定回调:

define_model_callbacks :update, :create, only: [:after, :before]

这将只创建 before_create / after_createbefore_update / after_update 回调,但跳过 around_* 回调。该选项将应用于该方法调用中定义的所有回调。可以多次调用 define_model_callbacks 以指定不同的生命周期事件:

define_model_callbacks :create, only: :after
define_model_callbacks :update, only: :before
define_model_callbacks :destroy, only: :around

这将只创建 after_createbefore_updatearound_destroy 方法。

1.6.2. 使用类定义回调

您可以将一个类传递给 before_<type>after_<type>around_<type>,以便更好地控制回调触发的时间和上下文。回调将触发该类的 <action>_<type> 方法,并将该类的一个实例作为参数传递。

class Person
  extend ActiveModel::Callbacks

  define_model_callbacks :create
  before_create PersonCallbacks
end

class PersonCallbacks
  def self.before_create(obj)
    # `obj` is the Person instance that the callback is being called on
  end
end

1.6.3. 中止回调

回调链可以通过抛出 :abort 在任何时候中止。这与 Active Record 回调的工作方式类似。

在下面的示例中,由于我们在 reset_me 方法中更新之前抛出 :abort,因此包括 before_update 在内的剩余回调链将被中止,并且 update 方法的主体将不会执行。

class Person
  extend ActiveModel::Callbacks

  define_model_callbacks :update

  before_update :reset_me
  after_update :finalize_me
  around_update :log_me

  def update
    run_callbacks(:update) do
      puts "update method called"
    end
  end

  private
    def reset_me
      puts "reset_me method: called before the update method"
      throw :abort
      puts "reset_me method: some code after abort"
    end

    def finalize_me
      puts "finalize_me method: called after the update method"
    end

    def log_me
      puts "log_me method: called around the update method"
      yield
      puts "log_me method: block successfully called"
    end
end
irb> person = Person.new

irb> person.update
reset_me method: called before the update method
=> false

1.7. 转换

ActiveModel::Conversion 是一系列方法,允许您将对象转换为不同的形式以用于不同目的。一个常见的用例是将对象转换为字符串或整数以构建 URL、表单字段等。

ActiveModel::Conversion 模块向类添加了以下方法:to_modelto_keyto_paramto_partial_path

方法的返回值取决于 persisted? 是否已定义以及是否提供了 id。如果对象已保存到数据库或存储,则 persisted? 方法应返回 true,否则应返回 falseid 应引用对象的 id,如果对象未保存,则为 nil。

class Person
  include ActiveModel::Conversion
  attr_accessor :id

  def initialize(id)
    @id = id
  end

  def persisted?
    id.present?
  end
end

1.7.1. to_model

to_model 方法返回对象本身。

irb> person = Person.new(1)
irb> person.to_model == person
=> true

如果您的模型行为不像 Active Model 对象,那么您应该自己定义 :to_model,返回一个代理对象,该对象用 Active Model 兼容方法包装您的对象。

class Person
  def to_model
    # A proxy object that wraps your object with Active Model compliant methods.
    PersonModel.new(self)
  end
end

1.7.2. to_key

无论对象是否持久化,如果设置了任何属性,to_key 方法都将返回对象关键属性的数组。如果没有关键属性,则返回 nil。

irb> person.to_key
=> [1]

关键属性是用于标识对象的属性。例如,在数据库支持的模型中,关键属性是主键。

1.7.3. to_param

to_param 方法返回对象键的 string 表示,适用于 URL 中使用,如果 persisted?false,则返回 nil

irb> person.to_param
=> "1"

1.7.4. to_partial_path

to_partial_path 方法返回一个 string,表示与对象关联的路径。Action Pack 使用它来查找合适的局部视图来表示对象。

irb> person.to_partial_path
=> "people/person"

1.8. 脏数据

ActiveModel::Dirty 对于在保存模型属性之前跟踪所做的更改很有用。此功能允许您确定哪些属性已修改,它们的先前值和当前值是什么,并根据这些更改执行操作。它对于应用程序中的审计、验证和条件逻辑特别方便。它提供了以与 Active Record 相同的方式跟踪对象更改的方法。

当一个对象的一个或多个属性发生更改但尚未保存时,它就变为脏数据。它具有基于属性的访问器方法。

要使用 ActiveModel::Dirty,您需要:

  1. 在您的类中包含该模块。
  2. 使用 define_attribute_methods 定义您要跟踪更改的属性方法。
  3. 在每次更改跟踪属性之前调用 [attr_name]_will_change!
  4. 在更改持久化后调用 changes_applied
  5. 当您要重置更改信息时调用 clear_changes_information
  6. 当您要恢复以前的数据时调用 restore_attributes

然后,您可以使用 ActiveModel::Dirty 提供的方法查询对象的所有更改属性列表、更改属性的原始值以及对属性所做的更改。

让我们考虑一个具有 first_namelast_name 属性的 Person 类,并确定如何使用 ActiveModel::Dirty 来跟踪这些属性的更改。

class Person
  include ActiveModel::Dirty

  attr_reader :first_name, :last_name
  define_attribute_methods :first_name, :last_name

  def initialize
    @first_name = nil
    @last_name = nil
  end

  def first_name=(value)
    first_name_will_change! unless value == @first_name
    @first_name = value
  end

  def last_name=(value)
    last_name_will_change! unless value == @last_name
    @last_name = value
  end

  def save
    # Persist data - clears dirty data and moves `changes` to `previous_changes`.
    changes_applied
  end

  def reload!
    # Clears all dirty data: current changes and previous changes.
    clear_changes_information
  end

  def rollback!
    # Restores all previous data of the provided attributes.
    restore_attributes
  end
end

1.8.1. 直接查询对象以获取其所有更改属性列表

irb> person = Person.new

# A newly instantiated `Person` object is unchanged:
irb> person.changed?
=> false

irb> person.first_name = "Jane Doe"
irb> person.first_name
=> "Jane Doe"

changed? 如果任何属性有未保存的更改,则返回 true,否则返回 false

irb> person.changed?
=> true

changed 返回一个数组,其中包含带有未保存更改的属性名称。

irb> person.changed
=> ["first_name"]

changed_attributes 返回一个哈希,其中包含带有未保存更改的属性,并指示它们的原始值,例如 attr => original value

irb> person.changed_attributes
=> {"first_name" => nil}

changes 返回一个更改哈希,其中属性名称作为键,值作为原始值和新值的数组,例如 attr => [original value, new value]

irb> person.changes
=> {"first_name" => [nil, "Jane Doe"]}

previous_changes 返回模型保存之前(即在调用 changes_applied 之前)已更改的属性的哈希。

irb> person.previous_changes
=> {}

irb> person.save
irb> person.previous_changes
=> {"first_name" => [nil, "Jane Doe"]}

1.8.2. 基于属性的访问器方法

irb> person = Person.new

irb> person.changed?
=> false

irb> person.first_name = "John Doe"
irb> person.first_name
=> "John Doe"

[attr_name]_changed? 检查特定属性是否已更改。

irb> person.first_name_changed?
=> true

[attr_name]_was 跟踪属性的先前值。

irb> person.first_name_was
=> nil

[attr_name]_change 跟踪更改属性的先前值和当前值。如果更改,则返回一个包含 [original value, new value] 的数组,否则返回 nil

irb> person.first_name_change
=> [nil, "John Doe"]
irb> person.last_name_change
=> nil

[attr_name]_previously_changed? 检查特定属性是否在模型保存之前(即在调用 changes_applied 之前)已更改。

irb> person.first_name_previously_changed?
=> false
irb> person.save
irb> person.first_name_previously_changed?
=> true

[attr_name]_previous_change 跟踪模型保存之前(即在调用 changes_applied 之前)更改属性的先前值和当前值。如果更改,则返回一个包含 [original value, new value] 的数组,否则返回 nil

irb> person.first_name_previous_change
=> [nil, "John Doe"]

1.9. 命名

ActiveModel::Naming 添加了一个类方法和辅助方法,使命名和路由更易于管理。该模块定义了 model_name 类方法,该方法将使用一些 ActiveSupport::Inflector 方法定义多个访问器。

class Person
  extend ActiveModel::Naming
end

name 返回模型的名称。

irb> Person.model_name.name
=> "Person"

singular 返回记录或类的单数类名。

irb> Person.model_name.singular
=> "person"

plural 返回记录或类的复数类名。

irb> Person.model_name.plural
=> "people"

element 删除命名空间并返回单数 snake_cased 名称。它通常由 Action Pack 和/或 Action View 助手用于帮助渲染部分/表单的名称。

irb> Person.model_name.element
=> "person"

human 使用 I18n 将模型名称转换为更人性化的格式。默认情况下,它将下划线化,然后人性化类名。

irb> Person.model_name.human
=> "Person"

collection 删除命名空间并返回复数 snake_cased 名称。它通常由 Action Pack 和/或 Action View 助手用于帮助渲染部分/表单的名称。

irb> Person.model_name.collection
=> "people"

param_key 返回用于参数名称的字符串。

irb> Person.model_name.param_key
=> "person"

i18n_key 返回 i18n 键的名称。它将模型名称下划线化,然后将其作为符号返回。

irb> Person.model_name.i18n_key
=> :person

route_key 返回用于生成路由名称的字符串。

irb> Person.model_name.route_key
=> "people"

singular_route_key 返回用于生成路由名称的字符串。

irb> Person.model_name.singular_route_key
=> "person"

uncountable? 识别记录或类的类名是否不可数。

irb> Person.model_name.uncountable?
=> false

一些 Naming 方法,如 param_keyroute_keysingular_route_key,对于命名空间模型,根据它是否在隔离的 Engine 内部而有所不同。

1.9.1. 自定义模型名称

有时您可能希望自定义在表单助手和 URL 生成中使用的模型名称。这在您希望为模型使用更友好的名称,同时仍能通过其完整命名空间引用它的情况下非常有用。

例如,假设您的 Rails 应用程序中有一个 Person 命名空间,并且您想为新的 Person::Profile 创建一个表单。

默认情况下,Rails 将生成 URL 为 /person/profiles 的表单,其中包含命名空间 person。但是,如果您希望 URL 简单地指向 profiles 而不带命名空间,您可以像这样自定义 model_name 方法:

module Person
  class Profile
    include ActiveModel::Model

    def self.model_name
      ActiveModel::Name.new(self, nil, "Profile")
    end
  end
end

通过此设置,当您使用 form_with 助手为创建新的 Person::Profile 创建表单时,Rails 将生成 URL 为 /profiles 而不是 /person/profiles 的表单,因为 model_name 方法已被覆盖以返回 Profile

此外,路径助手将不带命名空间生成,因此您可以使用 profiles_path 而不是 person_profiles_path 来生成 profiles 资源的 URL。要使用 profiles_path 助手,您需要在 config/routes.rb 文件中为 Person::Profile 模型定义路由,如下所示:

Rails.application.routes.draw do
  resources :profiles
end

因此,您可以预期模型为上一节中描述的方法返回以下值:

irb> name = ActiveModel::Name.new(Person::Profile, nil, "Profile")
=> #<ActiveModel::Name:0x000000014c5dbae0

irb> name.singular
=> "profile"
irb> name.singular_route_key
=> "profile"
irb> name.route_key
=> "profiles"

1.10. 安全密码

ActiveModel::SecurePassword 提供了一种以加密形式安全存储任何密码的方法。当您包含此模块时,会提供一个 has_secure_password 类方法,该方法默认定义一个带有某些验证的 password 访问器。

ActiveModel::SecurePassword 依赖于 bcrypt,因此请在您的 Gemfile 中包含此 gem 以使用它。

gem "bcrypt"

ActiveModel::SecurePassword 要求您具有 password_digest 属性。

自动添加以下验证:

  1. 密码在创建时必须存在。
  2. 密码确认(使用 password_confirmation 属性)。
  3. 密码的最大长度为 72 字节(必需,因为 bcrypt 在加密之前将字符串截断为该大小)。

如果不需要密码确认验证,只需省略 password_confirmation 的值(即,不要为其提供表单字段)。当此属性的值为 nil 时,验证将不会触发。

为了进一步自定义,可以通过传入 validations: false 作为参数来抑制默认验证。

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"

# `authenticate` is an alias for `authenticate_password`
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("aditya")
=> 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"

1.11. 序列化

ActiveModel::Serialization 为您的对象提供基本序列化。您需要声明一个包含您要序列化的属性的属性哈希。属性必须是字符串,而不是符号。

class Person
  include ActiveModel::Serialization

  attr_accessor :name, :age

  def attributes
    # Declaration of attributes that will be serialized
    { "name" => nil, "age" => nil }
  end

  def capitalized_name
    # Declared methods can be later included in the serialized hash
    name&.capitalize
  end
end

现在,您可以使用 serializable_hash 方法访问对象的序列化哈希。serializable_hash 的有效选项包括 :only:except:methods:include

irb> person = Person.new

irb> person.serializable_hash
=> {"name" => nil, "age" => nil}

# Set the name and age attributes and serialize the object
irb> person.name = "bob"
irb> person.age = 22
irb> person.serializable_hash
=> {"name" => "bob", "age" => 22}

# Use the methods option to include the capitalized_name method
irb>  person.serializable_hash(methods: :capitalized_name)
=> {"name" => "bob", "age" => 22, "capitalized_name" => "Bob"}

# Use the only method to include only the name attribute
irb> person.serializable_hash(only: :name)
=> {"name" => "bob"}

# Use the except method to exclude the name attribute
irb> person.serializable_hash(except: :name)
=> {"age" => 22}

利用 includes 选项的示例需要一个稍微复杂一些的场景,如下所示:

  class Person
    include ActiveModel::Serialization
    attr_accessor :name, :notes # Emulate has_many :notes

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

  class Note
    include ActiveModel::Serialization
    attr_accessor :title, :text

    def attributes
      { "title" => nil, "text" => nil }
    end
  end
irb> note = Note.new
irb> note.title = "Weekend Plans"
irb> note.text = "Some text here"

irb> person = Person.new
irb> person.name = "Napoleon"
irb> person.notes = [note]

irb> person.serializable_hash
=> {"name" => "Napoleon"}

irb> person.serializable_hash(include: { notes: { only: "title" }})
=> {"name" => "Napoleon", "notes" => [{"title" => "Weekend Plans"}]}

1.11.1. ActiveModel::Serializers::JSON

Active Model 还提供了 ActiveModel::Serializers::JSON 模块用于 JSON 序列化/反序列化。

要使用 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,提供了一个表示模型的哈希,其键为字符串。to_json 方法返回一个表示模型的 JSON 字符串。

irb> person = Person.new

# A hash representing the model with its keys as a string
irb> person.as_json
=> {"name" => nil}

# A JSON string representing the model
irb> person.to_json
=> "{\"name\":null}"

irb> person.name = "Bob"
irb> person.as_json
=> {"name" => "Bob"}

irb> person.to_json
=> "{\"name\":\"Bob\"}"

您还可以从 JSON 字符串定义模型的属性。为此,首先在您的类中定义 attributes= 方法:

class Person
  include ActiveModel::Serializers::JSON

  attr_accessor :name

  def attributes=(hash)
    hash.each do |key, value|
      public_send("#{key}=", value)
    end
  end

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

现在可以创建 Person 实例并使用 from_json 设置属性。

irb> json = { name: "Bob" }.to_json
=> "{\"name\":\"Bob\"}"

irb> person = Person.new

irb> person.from_json(json)
=> #<Person:0x00000100c773f0 @name="Bob">

irb> person.name
=> "Bob"

1.12. 翻译

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"
irb> Person.human_attribute_name("name")
=> "Name"

irb> I18n.locale = :"pt-BR"
=> :"pt-BR"
irb> Person.human_attribute_name("name")
=> "Nome"

1.13. 验证

ActiveModel::Validations 添加了验证对象的能力,这对于确保应用程序内的数据完整性和一致性很重要。通过将验证合并到模型中,您可以定义管理属性值正确性的规则,并防止无效数据。

class Person
  include ActiveModel::Validations

  attr_accessor :name, :email, :token

  validates :name, presence: true
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates! :token, presence: true
end
irb> person = Person.new
irb> person.token = "2b1f325"
irb> person.valid?
=> false

irb> person.name = "Jane Doe"
irb> person.email = "me"
irb> person.valid?
=> false

irb> person.email = "jane.doe@gmail.com"
irb> person.valid?
=> true

# `token` uses validate! and will raise an exception when not set.
irb> person.token = nil
irb> person.valid?
=> "Token can't be blank (ActiveModel::StrictValidationFailed)"

1.13.1. 验证方法和选项

您可以使用以下一些方法添加验证:

  • validate:通过方法或块向类添加验证。

  • validates:可以将属性传递给 validates 方法,它提供了所有默认验证器的快捷方式。

  • validates! 或设置 strict: true:用于定义最终用户无法更正且被视为异常的验证。每个使用感叹号或 :strict 选项设置为 true 定义的验证器在验证失败时将始终引发 ActiveModel::StrictValidationFailed,而不是添加到错误中。

  • validates_with:将记录传递给指定的一个或多个类,并允许它们根据更复杂的条件添加错误。

  • validates_each:针对一个块验证每个属性。

下面的一些选项可以与某些验证器一起使用。要确定您使用的选项是否可以与特定验证器一起使用,请通读 验证文档

  • :on:指定添加验证的上下文。您可以传递一个符号或一个符号数组。(例如 on: :createon: :custom_validation_contexton: [:create, :custom_validation_context])。没有 :on 选项的验证将无论上下文如何都运行。带有某些 :on 选项的验证将仅在指定的上下文运行。您可以在通过 valid?(:context) 验证时传递上下文。

  • :if:指定一个方法、proc 或字符串,用于确定是否应发生验证(例如 if: :allow_validation,或 if: -> { signup_step > 2 })。该方法、proc 或字符串应返回或评估为 truefalse 值。

  • :unless:指定一个方法、proc 或字符串,用于确定是否不应发生验证(例如 unless: :skip_validation,或 unless: Proc.new { |user| user.signup_step <= 2 })。该方法、proc 或字符串应返回或评估为 truefalse 值。

  • :allow_nil:如果属性为 nil,则跳过验证。

  • :allow_blank:如果属性为空,则跳过验证。

  • :strict:如果 :strict 选项设置为 true,它将引发 ActiveModel::StrictValidationFailed 而不是添加错误。:strict 选项也可以设置为任何其他异常。

在同一个方法上多次调用 validate 将覆盖以前的定义。

1.13.2. 错误

ActiveModel::Validations 自动向您的实例添加一个 errors 方法,该方法用新的 ActiveModel::Errors 对象初始化,因此您无需手动执行此操作。

在对象上运行 valid? 以检查对象是否有效。如果对象无效,它将返回 false,并且错误将添加到 errors 对象中。

irb> person = Person.new

irb> person.email = "me"
irb> person.valid?
=> # Raises Token can't be blank (ActiveModel::StrictValidationFailed)

irb> person.errors.to_hash
=> {:name => ["can't be blank"], :email => ["is invalid"]}

irb> person.errors.full_messages
=> ["Name can't be blank", "Email is invalid"]

1.14. Lint 测试

ActiveModel::Lint::Tests 允许您测试对象是否符合 Active Model API。通过在您的 TestCase 中包含 ActiveModel::Lint::Tests,它将包含一些测试,这些测试会告诉您您的对象是否完全符合,或者如果不符合,哪些方面的 API 未实现。

这些测试不尝试确定返回值的语义正确性。例如,您可以实现 valid? 以始终返回 true,并且测试将通过。您有责任确保这些值在语义上是有意义的。

您传入的对象应通过调用 to_model 返回一个符合要求的对象。to_model 返回 self 是完全可以的。

  • app/models/person.rb

    class Person
      include ActiveModel::API
    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


回到顶部