[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]
相关推荐
_UMR_29 分钟前
springboot集成Jasypt实现配置文件启动时自动解密-ENC
java·spring boot·后端
程序员小假35 分钟前
我们来说说 Cookie、Session、Token、JWT
java·后端
短剑重铸之日1 小时前
《SpringBoot4.0初识》第一篇:前瞻与思想
java·开发语言·后端·spring·springboot4.0
it_czz1 小时前
LangSmith vs LangFlow vs LangGraph Studio 可视化配置方案对比
后端
蓝色王者1 小时前
springboot 2.6.13 整合flowable6.8.1
java·spring boot·后端
花哥码天下2 小时前
apifox登录后设置token到环境变量
java·后端
hashiqimiya3 小时前
springboot事务触发滚动与不滚蛋
java·spring boot·后端
TeamDev3 小时前
基于 Angular UI 的 C# 桌面应用
前端·后端·angular.js
PPPHUANG3 小时前
一次 CompletableFuture 误用,如何耗尽 IO 线程池并拖垮整个系统
java·后端·代码规范