学习目标:理解 ActiveModel 如何为非 ActiveRecord 对象提供模型特性
引言#
ActiveModel 提供了一组模块,让任何 Ruby 对象都能表现得像 ActiveRecord 模型。
1. ActiveModel::Model#
1.1 基本用法#
class Person
include ActiveModel::Model
attr_accessor :name, :age
validates :name, presence: true
validates :age, numericality: true
end
person = Person.new(name: "John", age: 30)
person.valid? # => true
person.errors # => #<ActiveModel::Errors>
2. 核心模块#
2.1 Validations - 验证#
class User
include ActiveModel::Validations
attr_accessor :email, :password
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, length: { minimum: 6 }
validate :custom_validation
def custom_validation
errors.add(:email, "is taken") if email == "taken@example.com"
end
end
2.2 Callbacks - 回调#
class Person
extend ActiveModel::Callbacks
define_model_callbacks :save
before_save :normalize_name
after_save :send_notification
def save
run_callbacks :save do
# 保存逻辑
end
end
end
2.3 Dirty Tracking - 脏检查#
class Person
include ActiveModel::Dirty
define_attribute_methods :name
attr_reader :name
def name=(value)
name_will_change!
@name = value
end
end
person = Person.new
person.name = "John"
person.changed? # => true
person.name_changed? # => true
person.name_was # => nil
person.name_change # => [nil, "John"]
2.4 Attributes - 属性系统#
class Person
include ActiveModel::Attributes
attribute :name, :string
attribute :age, :integer, default: 18
attribute :born_at, :datetime
end
person = Person.new
person.age # => 18
3. 序列化#
3.1 Serialization#
class Person
include ActiveModel::Serialization
attr_accessor :name, :age
def attributes
{ 'name' => name, 'age' => age }
end
end
person = Person.new
person.name = "John"
person.serializable_hash
# => {"name" => "John", "age" => nil}
3.2 JSON 序列化#
class Person
include ActiveModel::Serializers::JSON
attr_accessor :name, :age
def attributes
{ 'name' => nil, 'age' => nil }
end
end
person = Person.new
person.name = "John"
person.age = 30
person.to_json
# => "{\"name\":\"John\",\"age\":30}"
4. 命名和转换#
4.1 Naming#
class Person
extend ActiveModel::Naming
end
Person.model_name.name # => "Person"
Person.model_name.singular # => "person"
Person.model_name.plural # => "people"
Person.model_name.route_key # => "people"
Person.model_name.param_key # => "person"
4.2 Conversion#
class Person
include ActiveModel::Conversion
attr_accessor :id
def persisted?
id.present?
end
end
person = Person.new
person.to_model # => person
person.to_key # => nil
person.to_param # => nil
person.id = 1
person.to_key # => [1]
person.to_param # => "1"
5. 表单对象模式#
5.1 实战示例#
class UserRegistration
include ActiveModel::Model
attr_accessor :email, :password, :password_confirmation, :accept_terms
validates :email, presence: true, email: true
validates :password, presence: true, length: { minimum: 6 }
validates :password_confirmation, presence: true
validates :accept_terms, acceptance: true
validate :passwords_match
def save
return false unless valid?
User.transaction do
user = User.create!(email: email, password: password)
UserMailer.welcome(user).deliver_later
true
end
rescue ActiveRecord::RecordInvalid
false
end
private
def passwords_match
return if password == password_confirmation
errors.add(:password_confirmation, "doesn't match password")
end
end
# 在控制器中使用
def create
@registration = UserRegistration.new(registration_params)
if @registration.save
redirect_to root_path, notice: "Welcome!"
else
render :new
end
end
6. 自定义验证器#
6.1 验证器类#
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
record.errors.add(attribute, options[:message] || "is not a valid email")
end
end
end
class User
include ActiveModel::Validations
attr_accessor :email
validates :email, email: true
end
7. 本章总结#
ActiveModel 提供了丰富的模块,让你能够:
- 为任何 Ruby 对象添加验证
- 实现表单对象模式
- 支持序列化和转换
- 使用回调和脏检查
8. 练习题#
- 创建一个表单对象处理复杂的注册流程
- 实现一个自定义验证器
- 使用 ActiveModel 创建一个 PORO(Plain Old Ruby Object)模型
上一篇:ActiveRecord 关联关系 下一篇:ActionPack 概览 返回:学习指南首页