[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]
相关推荐
程序媛小果20 分钟前
基于Spring Boot+vue的厨艺交流平台系统设计与实现
vue.js·spring boot·后端
Aska_Lv1 小时前
线上问题-我就加了个索引怎么就导致线上事故了
后端
liangblog2 小时前
在Spring Boot项目中导出复杂对象到Excel文件
spring boot·后端·excel
凌览3 小时前
想找免费又强大的消息推送服务?MoePush 能满足你吗?
前端·后端·github
懒麻蛇4 小时前
使用R包ellmer接入Deepseek
开发语言·后端·python·r语言·flask
chi_6665 小时前
Spring Boot的启动流程
java·spring boot·后端
计算机人哪有不疯的8 小时前
【虚拟机 IP 配置深度剖析】
开发语言·后端·spark
乔大将军13 小时前
项目准备(flask+pyhon+MachineLearning)- 1
后端·python·flask