[ruby on rails]ActiveModel源码阅读(Validations)

Validations

  • 类方法
  1. validate 自定义验证方法 (不需要属性,也不需要校验器;直接做校验)
ruby 复制代码
class Invoice < ApplicationRecord
 validate :active_customer, on: :create

 def active_customer
   errors.add(:customer_id, "is not active") unless customer.active?
 end
end
  1. validates 和 validates! (需要属性,也需要校验器;间接做校验)
ruby 复制代码
class Person < ApplicationRecord
  validates :name, presence: true #校验器是PresenceValidator
end

#校验器PresenceValidator
class PresenceValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors.add attribute, "can't be blank" if value.blank?
  end
end
 
  1. validates_each (需要属性,不需要校验器;直接做校验)
ruby 复制代码
class Person < ApplicationRecord
  validates_each :name, :surname do |record, attr, value|
    record.errors.add(attr, 'must start with upper case') if value =~ /\A[[:lower:]]/
  end
end
  1. validates_with 验证交给其他类做验证 (不需要属性,需要校验器;作用于所有对象,间接做校验)
ruby 复制代码
class GoodnessValidator < ActiveModel::Validator
  def validate(record)
  	# options = { :fields => [:first_name, :last_name] }
  	# options = { :abc => [:first_name, :last_name] }
    if options[:fields].any?{|field| record.send(field) == "Evil" }
      record.errors[:base] << "This person is evil"
    end
  end
end
 
class Person < ApplicationRecord
	#fields是options哈希的一个key,也可以是别的例如 abc: [:first_name, :last_name]
  validates_with GoodnessValidator, fields: [:first_name, :last_name]
end

经验:牢记 validate 发生在 save 之前,如果你喜欢用 before_save 之类的进行检验,记得加上 return false 或者改变习惯,用 validate :validate_method 类似写法进行校验。

Validator

  • 自定义验证类(校验器)
  1. 自定义的验证类继承自 ActiveModel::Validator,必须实现 validate 方法,其参数是要验证的记录,然后验证这个记录是否有效。自定义的验证类通过 validates_with 方法调用
ruby 复制代码
class MyValidator < ActiveModel::Validator
  def validate(record)
    unless record.name.starts_with? 'X'
      record.errors[:name] << 'Need a name starting with X please!'
    end
  end
end
 
class Person
  include ActiveModel::Validations
  validates_with MyValidator
end
  1. 自定义的验证类中验证单个属性,最简单的方法是继承 ActiveModel::EachValidator 类。此时,自定义的验证类必须实现 validate_each 方法。这个方法接受三个参数:记录、属性名和属性值。它们分别对应模型实例、要验证的属性及其值。
ruby 复制代码
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 < ApplicationRecord
  # presence: true为内置的验证类,email: true为自定义的验证类,可以同时使用
  # email: true 对应的验证类必须是EmailValidator, title:true 对应的验证类必须是TitlelValidator
  validates :email, presence: true, email: true
end
  • 自定义验证方法
    自定义验证方法必须使用类方法 validate 注册,传入自定义验证方法名的符号形式
    这个类方法可以接受多个符号,自定义的验证方法会按照注册的顺序执行
    可以设置 :on 选项,指定什么时候验证。:on 的可选值为 :create 和 :update。
ruby 复制代码
class Invoice < ApplicationRecord
  validate :expiration_date_cannot_be_in_the_past,
    :discount_cannot_be_greater_than_total_value
 
  def expiration_date_cannot_be_in_the_past
    if expiration_date.present? && expiration_date < Date.today
      errors.add(:expiration_date, "can't be in the past")
    end
  end
 
  def discount_cannot_be_greater_than_total_value
    if discount > total_value
      errors.add(:discount, "can't be greater than total value")
    end
  end
end
  • valid? 校验是否通过,只有一个判断规则:当前 record 对象的 errors 值是否为空 errors.empty?
    所以自己写校验方法或者校验器的时候,请记得设置 errors 的值。

Concern实现

ruby 复制代码
# app/models/concerns/custom_validatable.rb
module CustomValidatable
  extend ActiveSupport::Concern

  included do
    validate :custom_validate
  end

  def validate_custom_fields(fields)
    CustomValidation.order(:position).each do |validation|
      regex = Regexp.new(validation.regex)
      fields.each do |field|
        value = send(field)

        unless value.match?(regex)
          errors.add(field, validation.error_message)
        end
      end
    end
  end
end
ruby 复制代码
# app/models/issue.rb
include CustomValidatable
def custom_validate
  validate_custom_fields(['subject', 'description'])
end

validates_with实现

ruby 复制代码
class WxMsgSecValidator < ActiveModel::Validator
  # scene: 1 scene_profile 资料, 2 scene_comment 评论, 3 scene_post 论坛, 4 scene_log 社交日志
  def validate(record)
    if options[:fields].select { |field| record.send("#{field}_changed?") }.present?
      case record.class.name
      when 'YouKe'
        @you_ke = record
      else
        @you_ke = record.you_ke
      end
      scene_hash = { desc: 2, content: 2 }
      @you_ke = YouKe.where(is_robot: false).first if @you_ke&.is_robot?
      open_id = @you_ke&.klly? ? @you_ke&.open_id : @you_ke&.wmh_open_id
      return record.errors[:base] << '账号openid不存在' if open_id.nil?

      options[:fields].select { |field| record.send("#{field}_changed?") }.each do |field|
        if record.send(field).present?
          is_deny = @you_ke.klly? ? !Weixin.msg_check(scene_hash[field], open_id, record.send(field)) : !WmhWeixin.msg_check(scene_hash[field], open_id, record.send(field))
          record.errors.add(:base, '输入涉嫌违规') if is_deny
        end
      end
    end
  end
end
ruby 复制代码
# app/models/issue.rb
 include ActiveModel::Validations
 validates_with WxMsgSecValidator, fields: %i[content]
相关推荐
kfaino34 分钟前
码农的AI翻身(六)你好,我叫 Parameter
后端·aigc
掘金者阿豪37 分钟前
把业务数据变成共享仪表盘:Metabase可视化与远程访问实践
前端·后端
猪猪拆迁队2 小时前
虚拟工厂仿真引擎的架构设计:让一条产线可编程、可观测、可干预
后端·ai编程
字节跳动数据库2 小时前
文章分享——相似函数处理方法
人工智能·后端·程序员
云技纵横2 小时前
@Transactional 失效的 7 种场景:第 5 种最难排查
后端
用户6757049885022 小时前
你知道 Go 结构体和结构体指针调用的区别吗?一文带你彻底搞懂!
后端·go
程序员cxuan3 小时前
读懂 Claude Code 架构分析系列,第一篇,开始!
人工智能·后端·架构
用户6757049885023 小时前
面试官问“装饰器模式”,这样回答薪资多要 3000!
后端
tntxia3 小时前
Geo Scene域名修改引起的一些问题
后端
用户298698530143 小时前
Java 实现 Word 文档加密与权限解除
java·后端