关于 active-model
Active Model 技能用于构建无需数据库持久化、但可接入 Rails 表单、验证和序列化的 Ruby 对象,帮助 Rails 开发者选择模块并实现表单、搜索及配置对象。
主要功能
- 01兼容 Rails 的非持久化模型
- 02类型化属性与默认值
- 03验证和生命周期回调
- 04变更跟踪与序列化
- 05表单及搜索对象模式
适用场景
- 01创建聚合多模型的表单对象
- 02构建搜索和筛选表单
- 03验证配置对象
- 04序列化 API 响应对象
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill active-model安装后在 Claude Code 中输入/active-model确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/api-and-attributes.mddirty-tracking.mdpatterns.mdserialization.mdvalidations-and-callbacks.md详细介绍
Active Model 专家
构建类似模型的 Ruby 类,使其能够与 Rails 表单、验证和序列化配合使用,而无需接触数据库。
核心选择:Active Model 还是 Active Record
在以下情况下使用 Active Model:
- 对象没有对应的数据库表
- 聚合多个模型的表单对象
- 搜索/筛选表单
- API 请求/响应封装对象
- 配置或设置对象
- 联系表单、邀请表单、入门引导向导
- 需要验证功能的装饰器或展示器
- 需要验证和错误消息的服务对象
在以下情况下使用 Active Record:
- 数据必须持久化到数据库
- 需要关联、作用域或查询接口
- 需要迁移和架构管理
Agent 最常犯的错误: 明明 Active Model 已能免费提供 Rails 表单和控制器所需的一切,却仍选择 Active Record(或手动编写验证逻辑的原始 PORO)。
模块层级结构
了解应该包含哪个模块至关重要:
| 模块 | 提供的功能 | 适用场景 |
|---|---|---|
ActiveModel::Model | API + 后续扩展 | 默认选择 — 使用此模块 |
ActiveModel::API | 验证、命名、转换、翻译、属性赋值 | Model 的轻量替代方案 |
ActiveModel::Attributes | 带类型转换和默认值的类型化属性 | 需要类型强制转换(日期、布尔值、整数) |
ActiveModel::Validations | 仅验证功能 | 为任意对象添加验证 |
ActiveModel::Callbacks | 生命周期钩子(before/after/around) | 需要回调链 |
ActiveModel::Dirty | 变更跟踪 | 跟踪属性修改 |
ActiveModel::Serialization | serializable_hash | 需要哈希/JSON 输出 |
ActiveModel::SecurePassword | bcrypt 密码处理 | 无需 Active Record 即可处理密码 |
关键见解: ActiveModel::Model 包含 ActiveModel::API,后者集成了验证、命名、转换、翻译和属性赋值功能。先从 Model 开始,再按需添加其他模块。
使用说明
步骤 1:选择合适的基类
对于大多数表单对象和虚拟模型,请使用 ActiveModel::Model:
class ContactForm
include ActiveModel::Model
attr_accessor :name, :email, :message
validates :name, :email, :message, presence: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
def submit
return false unless valid?
ContactMailer.new_message(name:, email:, message:).deliver_later
true
end
end
它可以立即与 form_with、render 以及所有 Action View 帮助方法配合使用。
需要类型化属性时,添加 ActiveModel::Attributes:
class SearchForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :query, :string
attribute :min_price, :decimal
attribute :max_price, :decimal
attribute :available_only, :boolean, default: false
attribute :created_after, :date
validates :query, length: { minimum: 2 }, allow_blank: true
def results
scope = Product.all
scope = scope.where("name ILIKE ?", "%#{query}%") if query.present?
scope = scope.where("price >= ?", min_price) if min_price.present?
scope = scope.where("price <= ?", max_price) if max_price.present?
scope = scope.where(available: true) if available_only
scope = scope.where("created_at >= ?", created_after) if created_after.present?
scope
end
end
Attributes 会自动进行类型转换 — 字符串 "true" 会变成布尔值 true,字符串 "2024-01-15" 会变成 Date。
步骤 2:接入控制器
Active Model 对象在控制器中的用法与 Active Record 完全相同:
class ContactFormsController < ApplicationController
def new
@contact_form = ContactForm.new
end
def create
@contact_form = ContactForm.new(contact_form_params)
if @contact_form.submit
redirect_to root_path, notice: "Message sent!"
else
render :new, status: :unprocessable_entity
end
end
private
def contact_form_params
params.require(:contact_form).permit(:name, :email, :message)
end
end
步骤 3:与表单配合使用
<%= form_with model: @contact_form, url: contact_forms_path do |f| %>
<% if @contact_form.errors.any? %>
<div id="errors">
<% @contact_form.errors.full_messages.each do |msg| %>
<p><%= msg %></p>
<% end %>
</div>
<% end %>
<%= f.text_field :name %>
<%= f.email_field :email %>
<%= f.text_area :message %>
<%= f.submit "Send" %>
<% end %>
关键: 必须在 form_with 中提供 url:,因为 Active Model 对象默认不可路由(没有返回 true 的 persisted?,也没有 id)。
步骤 4:按需添加模块
只包含实际用到的模块。不要盲目照搬并添加所有模块。
回调 — 需要生命周期钩子时:
class RegistrationForm
include ActiveModel::Model
include ActiveModel::Attributes
extend ActiveModel::Callbacks
define_model_callbacks :save
attribute :email, :string
attribute :name, :string
attribute :company_name, :string
before_save :normalize_email
after_save :send_welcome_email
def save
return false unless valid?
run_callbacks(:save) do
create_records!
end
true
end
private
def normalize_email
self.email = email.downcase.strip
end
def send_welcome_email
WelcomeMailer.registration(@user).deliver_later
end
def create_records!
@company = Company.create!(name: company_name)
@user = @company.users.create!(email:, name:)
end
end
重要: 对于 Callbacks,要使用 extend,而不是 include。此外,必须自行调用 run_callbacks(:event) { ... } — Active Model 不会自动触发回调。
变更跟踪 — 需要检测变更时:
class Settings
include ActiveModel::Model
include ActiveModel::Dirty
define_attribute_methods :theme, :language
def theme
@theme
end
def theme=(value)
theme_will_change! unless value == @theme
@theme = value
end
def language
@language
end
def language=(value)
language_will_change! unless value == @language
@language = value
end
def save
changes_applied
end
def reload!
clear_changes_information
end
end
序列化 — 用于 JSON API:
class ApiResponse
include ActiveModel::Model
include ActiveModel::Serializers::JSON
attr_accessor :status, :data, :timestamp
def attributes
{ "status" => nil, "data" => nil, "timestamp" => nil }
end
end
response = ApiResponse.new(status: "ok", data: { count: 42 }, timestamp: Time.current)
response.as_json # => {"status"=>"ok", "data"=>{"count"=>42}, "timestamp"=>"2024-..."}
response.to_json # => '{"status":"ok",...}'
SecurePassword — 无需 Active Record 即可处理密码:
class SessionForm
include ActiveModel::Model
include ActiveModel::SecurePassword
has_secure_password
attr_accessor :password_digest
def authenticate_user(email, password)
user = User.find_by(email:)
user&.authenticate(password)
end
end
需要 bcrypt gem。会提供 password、password_confirmation 和 authenticate 方法。
步骤 5:命名和翻译
ActiveModel::Model 会自动包含 Naming 和 Translation。
自定义模型名称(适用于带命名空间的类):
module Admin
class InviteForm
include ActiveModel::Model
def self.model_name
ActiveModel::Name.new(self, nil, "InviteForm")
end
end
end
# Without override: form params would be admin_invite_form[email]
# With override: form params are invite_form[email]
属性名称的 I18n:
# config/locales/en.yml
en:
activemodel:
attributes:
contact_form:
name: "Full Name"
email: "Email Address"
errors:
models:
contact_form:
attributes:
email:
invalid: "doesn't look right"
步骤 6:测试 Active Model 对象
require "test_helper"
class ContactFormTest < ActiveSupport::TestCase
test "valid with all attributes" do
form = ContactForm.new(name: "Jane", email: "[email protected]", message: "Hello")
assert form.valid?
end
test "invalid without name" do
form = ContactForm.new(email: "[email protected]", message: "Hello")
refute form.valid?
assert_includes form.errors[:name], "can't be blank"
end
test "invalid with bad email" do
form = ContactForm.new(name: "Jane", email: "not-an-email", message: "Hello")
refute form.valid?
assert_includes form.errors[:email], "is invalid"
end
test "#submit delivers email when valid" do
form = ContactForm.new(name: "Jane", email: "[email protected]", message: "Hello")
assert_enqueued_emails 1 do
assert form.submit
end
end
test "#submit returns false when invalid" do
form = ContactForm.new
refute form.submit
end
end
Lint 测试 — 验证 API 合规性:
class ContactFormLintTest < ActiveSupport::TestCase
include ActiveModel::Lint::Tests
setup do
@model = ContactForm.new
end
end
常见模式
表单对象(多模型)
class RegistrationForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :user_email, :string
attribute :user_name, :string
attribute :company_name, :string
attribute :plan, :string, default: "free"
validates :user_email, :user_name, :company_name, presence: true
validates :user_email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :plan, inclusion: { in: %w[free pro enterprise] }
def save
return false unless valid?
ActiveRecord::Base.transaction do
company = Company.create!(name: company_name)
company.users.create!(email: user_email, name: user_name)
company.subscriptions.create!(plan:)
end
true
rescue ActiveRecord::RecordInvalid => e
errors.add(:base, e.message)
false
end
end
搜索/筛选表单
class OrderSearch
include ActiveModel::Model
include ActiveModel::Attributes
attribute :status, :string
attribute :customer_name, :string
attribute :date_from, :date
attribute :date_to, :date
attribute :min_total, :decimal
attribute :sort_by, :string, default: "created_at"
attribute :sort_direction, :string, default: "desc"
def results
scope = Order.includes(:customer)
scope = scope.where(status:) if status.present?
scope = scope.joins(:customer).where("customers.name ILIKE ?", "%#{customer_name}%") if customer_name.present?
scope = scope.where("orders.created_at >= ?", date_from) if date_from.present?
scope = scope.where("orders.created_at <= ?", date_to) if date_to.present?
scope = scope.where("orders.total >= ?", min_total) if min_total.present?
scope = scope.order(sort_by => sort_direction)
scope
end
end
配置对象
class NotificationPreferences
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Serializers::JSON
attribute :email_enabled, :boolean, default: true
attribute :sms_enabled, :boolean, default: false
attribute :digest_frequency, :string, default: "daily"
attribute :quiet_hours_start, :integer, default: 22
attribute :quiet_hours_end, :integer, default: 8
validates :digest_frequency, inclusion: { in: %w[realtime hourly daily weekly] }
validates :quiet_hours_start, :quiet_hours_end,
numericality: { in: 0..23 }
def attributes
{
"email_enabled" => nil,
"sms_enabled" => nil,
"digest_frequency" => nil,
"quiet_hours_start" => nil,
"quiet_hours_end" => nil
}
end
def quiet_now?
hour = Time.current.hour
if quiet_hours_start > quiet_hours_end
hour >= quiet_hours_start || hour < quiet_hours_end
else
hour >= quiet_hours_start && hour < quiet_hours_end
end
end
end
反模式
- 对非持久化对象使用 Active Record — 如果没有数据表,就不要继承
ApplicationRecord - 手动编写验证逻辑 — 已有
validates :name, presence: true时,不要写raise "Name required" if name.blank? - 跳过
ActiveModel::Model— 不要手动实现通过哈希遍历的initialize(attrs={});Model会处理 - 包含所有模块 — 只包含实际使用的模块;大多数情况下,
Model就足够了 - 在
form_with中忘记url:— Active Model 对象不会自动解析路由 - 需要
include时使用extend— 回调使用extend;其他情况使用include - 未在事务中封装多个模型的保存操作 — 创建多条记录的表单对象需要使用
ActiveRecord::Base.transaction - 重新实现
assign_attributes—ActiveModel::Model已通过初始化方法提供从哈希赋值属性的功能
参考
有关详细模式、边缘情况和高级用法,请参阅 references/ 目录:
references/api-and-attributes.md— Model 与 API、类型化属性、命名、翻译、转换references/validations-and-callbacks.md— 所有验证器、自定义验证器、回调、错误处理references/dirty-tracking.md— 手动和自动变更跟踪references/serialization.md— serializable_hash、JSON 序列化references/patterns.md— 表单对象、向导、服务对象、lint 测试、边缘情况