首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

ActiveRecord 驗證與回調深度解析:從內置規則到自定義驗證器的最佳實踐

     編輯:Java 相關知識

對象生命週期與驗證觸發時機

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

自定義驗證擴展

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[attribute] << (options[:message] || "is not an email")
    end
  end
end

# 使用
class Person < ActiveRecord::Base
  validates :email, presence: true, email: true
end

2. 驗證方法(內聚於模型)

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

3. 類宏擴展(DRY)

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

回調:參與對象生命週期

18 個成對回調 + 2 個特殊回調

# 生命週期順序
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

擴展 Base 實現聲明式加密

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

Observer:橫切關注點分離

回調易導致模型膨脹(如 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)才會激活。
  • 實現了 AOP(面向切面編程),將橫切邏輯從核心模型剝離。

總結

驗證保證數據完整性,回調/觀察器編排業務流程。最佳實踐:

  1. 優先使用內置驗證器,覆蓋 90% 場景。
  2. 複雜驗證提取為 EachValidator,跨模型復用。
  3. 條件驗證用 if/unless: :method,保持可讀性。
  4. 回調用聲明式處理器,避免實例方法污染模型接口。
  5. 發郵件、日志、緩存等副作用移至 Observer/Service Object,模型保持單一職責。
  6. after_find/after_initialize 必須聲明空方法 以激活回調。

「胖模型」不等於「把所有邏輯塞進模型」,而是「模型封裝領域不變量,協調器編排用例流程」。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved