ActiveRecord 對象經歷:new → save/create → update → destroy。驗證在 發送 INSERT/UPDATE SQL 前 自動運行。觸發驗證的方法:
create、create!、save、save!、update、update_attributes、update_attributes!save(validate: false)、update_column、toggle!、increment! 等「危險方法」慎用。| 驗證器 | 用途 | 關鍵選項 |
|---|---|---|
presence | 非空(非 nil/空字符串/空白字符) | |
length | 長度範圍 | minimum, maximum, in: 6..20, is: 6, tokenizer(按詞計數) |
format | 正則匹配 | with: /\A[a-z]+\z/, without |
numericality | 數值類型 | only_integer, greater_than, less_than_or_equal_to, odd/even |
inclusion | 值在白名單中 | in: %w[small medium large] (within 別名) |
exclusion | 值不在黑名單中 | in: %w[admin root] |
uniqueness | 值唯一 | scope: :year(組合唯一), case_sensitive: false |
acceptance | 勾選同意(虛擬屬性) | accept: 'yes'(默認 '1') |
confirmation | 兩次輸入一致 | 生成 attr_confirmation 虛擬屬性,需配合 presence |
validates_associated | 關聯對象驗證 | 避免雙向調用導致無限循環 |
on: :create / :update / :save:限定觸發場景(默認 :save)。message: "custom error":自定義錯誤消息(支持 %{value}、%{attribute} 插值)。allow_nil: true:允許 nil 跳過驗證。allow_blank: true:允許 blank?(nil/空串/空白)跳過。if: :method_name / Proc / "string":條件驗證。unless: ...:反向條件。
class Order < ActiveRecord::Base
validates :card_number, presence: true, if: :paid_with_card?
def paid_with_card?
payment_type == "card"
end
end
class Account < ActiveRecord::Base
validates :password, confirmation: true, unless: Proc.new { |a| a.password.blank? }
end
class User < ActiveRecord::Base
with_options if: :is_admin? do |admin|
admin.validates :password, length: { minimum: 10 }
admin.validates :email, presence: true
end
end
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
record.errors[attribute] << (options[:message] || "is not an email")
end
end
end
# 使用
class Person < ActiveRecord::Base
validates :email, presence: true, email: true
end
class Invoice < ActiveRecord::Base
validate :expiration_date_cannot_be_in_the_past, :discount_cannot_exceed_total
def expiration_date_cannot_be_in_the_past
errors.add(:expiration_date, "can't be in the past") if expiration_date.present? && expiration_date < Date.today
end
def discount_cannot_exceed_total
errors.add(:discount, "can't exceed total") if discount > total_value
end
end
ActiveRecord::Base.class_eval do
def self.validates_as_choice(attr_name, n, options = {})
validates attr_name, { inclusion: { in: 1..n } }.merge(options)
end
end
class Movie < ActiveRecord::Base
validates_as_choice :rating, 5
end
# 生命週期順序 before_validation → after_validation before_save → around_save → after_save before_create → around_create → after_create before_update → around_update → after_update before_destroy → around_destroy → after_destroy after_find / after_initialize(特殊:只能定義為實例方法)

方式一:實例方法
class Order < ActiveRecord::Base
def after_save
self.payment_due ||= Time.now + 30.days
end
end
方式二:聲明式處理器(推薦,支持多處理器、順序控制、可復用)
class Order < ActiveRecord::Base
before_validation :normalize_credit_card_number
after_create do |order|
logger.info "Order #{order.id} created"
end
protected
def normalize_credit_card_number
self.cc_number.gsub!(/[-\s]/, '')
end
end
false 中斷鏈。after_find/after_initialize 必須定義為實例方法(哪怕是空方法),否則不會被調用。
class CreditCardCallbacks
def before_validation(model)
model.cc_number.gsub!(/[-\s]/, '')
end
end
class Order < ActiveRecord::Base
before_validation CreditCardCallbacks.new
end
class Subscription < ActiveRecord::Base
before_validation CreditCardCallbacks.new
end
class Encrypter
def initialize(attrs)
@attrs = attrs
end
def before_save(model)
@attrs.each { |f| model[f].tr!('a-zA-Z', 'b-zaB-ZA') }
end
def after_save(model)
@attrs.each { |f| model[f].tr!('b-zaB-ZA', 'a-zA-Z') }
end
alias_method :after_find, :after_save
end
class Order < ActiveRecord::Base
encrypter = Encrypter.new([:name, :email])
before_save encrypter
after_save encrypter
after_find encrypter
def after_find; end # 必須聲明,觸發 after_find 回調
end
class ActiveRecord::Base
def self.encrypt(*attrs)
encrypter = Encrypter.new(attrs)
before_save encrypter
after_save encrypter
after_find encrypter
define_method(:after_find) { }
end
end
class Order < ActiveRecord::Base
encrypt :name, :email
end
回調易導致模型膨脹(如 after_save 發郵件、寫日志、清緩存)。Observer 透明注入行為,不修改模型代碼。
class OrderObserver < ActiveRecord::Observer
def after_save(order)
order.logger.info "Order #{order.id} created"
end
end
class AuditObserver < ActiveRecord::Observer
observe Order, Payment, Refund
def after_save(model)
model.logger.info "[Audit] #{model.class} #{model.id} created"
end
end
# config/application.rb
config.active_record.observers = :order_observer, :audit_observer
app/models,自動匹配同名模型(OrderObserver → Order)。OrderObserver.instance)才會激活。驗證保證數據完整性,回調/觀察器編排業務流程。最佳實踐:
if/unless: :method,保持可讀性。「胖模型」不等於「把所有邏輯塞進模型」,而是「模型封裝領域不變量,協調器編排用例流程」。