更多内容请访问 rubyonrails.org:

Active Model 基础

本指南将为你提供使用 Active Model 入门所需的知识。Active Model 提供了一种方法,使 Action Pack 和 Action View 助手能够与普通的 Ruby 对象进行交互。它还有助于构建自定义 ORM,以便在 Rails 框架之外使用。

阅读完本指南后,你将了解

  • 什么是 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: "[email protected]", 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 的类都可以与 form_withrender 以及其他 Action View 助手方法 一同使用,就像 Active Record 对象一样。

例如,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 表单助手布局和渲染 指南中分别了解有关如何与 ActiveModel::API 兼容的对象一起使用 form_withrender 的更多信息。

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? 用于 强参数 集成,你将从请求中分配一个 params 属性。

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. 在您的类中,您可以像在 Active Record 模型中使用它们一样使用 before_updateafter_updatearound_update 方法。
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? 是否定义以及是否提供了 idpersisted? 方法如果对象已保存到数据库或存储中,则应返回 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_namePerson 类,并确定如何使用 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 SecurePassword

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 还为 JSON 序列化/反序列化提供了 ActiveModel::Serializers::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

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

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 = "[email protected]"
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


返回顶部