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 Pack 和 Action 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_with
、render
以及其他 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_with
和 render
的更多信息。
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_attributes
和 attributes=
都是方法调用,它们接受要分配的属性的哈希作为参数。在许多情况下,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
提供了一种为模型属性动态定义方法的方式。该模块特别有用,可以简化属性访问和操作,并且可以为类的 方法添加自定义前缀和后缀。您可以定义前缀和后缀,以及对象上的哪些方法将使用它们,如下所示
- 在您的类中包含
ActiveModel::AttributeMethods
。 - 调用您要添加的每个方法,例如
attribute_method_suffix
、attribute_method_prefix
、attribute_method_affix
。 - 在其他方法之后调用
define_attribute_methods
,以声明应添加前缀和后缀的属性。 - 定义您已声明的各种通用
_attribute
方法。这些方法中的参数attribute
将被define_attribute_methods
中传递的参数替换。在下面的示例中,它是name
。
attribute_method_prefix
和 attribute_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_update
和 after_create
,以及定义在模型生命周期的特定点执行的自定义逻辑。
您可以按照以下步骤实现 ActiveModel::Callbacks
- 在您的类中扩展
ActiveModel::Callbacks
。 - 使用
define_model_callbacks
建立一个应该与其关联的回调方法的列表。当您指定一个方法(如:update
)时,它将自动包含:update
事件的所有三个默认回调(before
、around
和after
)。 - 在定义的方法中,使用
run_callbacks
,它将在触发特定事件时执行回调链。 - 在您的类中,您可以像在 Active Record 模型中使用它们一样使用
before_update
、after_update
和around_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_callbacks
的 method_name
必须不以 !
、?
或 =
结尾。此外,多次定义相同的回调将覆盖之前的回调定义。
1.6.1 定义特定回调
您可以选择通过将 only
选项传递给 define_model_callbacks
方法来创建特定回调
define_model_callbacks :update, :create, only: [:after, :before]
这将仅创建 before_create
/ after_create
和 before_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_create
、before_update
和 around_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_model
、to_key
、to_param
和 to_partial_path
。
方法的返回值取决于 persisted?
是否定义以及是否提供了 id
。persisted?
方法如果对象已保存到数据库或存储中,则应返回 true
,否则应返回 false
。id
应引用对象的 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
,您需要
- 在您的类中包含该模块。
- 使用
define_attribute_methods
定义要跟踪更改的属性方法。 - 在对跟踪的属性进行每次更改之前调用
[attr_name]_will_change!
。 - 在更改持久化后调用
changes_applied
。 - 当您想要重置更改信息时,调用
clear_changes_information
。 - 当您想要恢复先前数据时,调用
restore_attributes
。
然后,您可以使用 ActiveModel::Dirty
提供的方法来查询对象的全部更改属性列表、更改属性的原始值以及对属性所做的更改。
让我们考虑一个具有属性 first_name
和 last_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_key
、route_key
和 singular_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
属性。
以下验证将自动添加
- 创建时密码必须存在。
- 密码确认(使用
password_confirmation
属性)。 - 密码的最大长度为 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: :create
或on: :custom_validation_context
或on: [:create, :custom_validation_context]
)。没有:on
选项的验证将在任何上下文中运行。具有:on
选项的验证仅在指定的上下文中运行。您可以在验证时通过valid?(:context)
传递上下文。:if
: 指定要调用的方法、proc 或字符串,以确定是否应该执行验证(例如if: :allow_validation
,或if: -> { signup_step > 2 }
)。该方法、proc 或字符串应该返回或评估为true
或false
值。:unless
: 指定要调用的方法、proc 或字符串,以确定是否不应执行验证(例如unless: :skip_validation
,或unless: Proc.new { |user| user.signup_step <= 2 }
)。该方法、proc 或字符串应该返回或评估为true
或false
值。: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