`
hideto
  • 浏览: 2666716 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Rails源码研究之ActiveRecord:四,Validations

    博客分类:
  • Ruby
阅读更多
Validations相关的源码全在validations.rb文件里:
module ActiveRecord
  class Errors
    include Enumerable

    @@default_error_messages = {
      :inclusion => "is not included in the list",
      :exclusion => "is reserved",
      :invalid => "is invalid",
      :confirmation => "doesn't match confirmation",
      :accepted  => "must be accepted",
      :empty => "can't be empty",
      :blank => "can't be blank",
      :too_long => "is too long (maximum is %d characters)",
      :too_short => "is too short (minimum is %d characters)",
      :wrong_length => "is the wrong length (should be %d characters)",
      :taken => "has already been taken",
      :not_a_number => "is not a number"
    }

    def add(attribute, msg = @@default_error_messages[:invalid])
      @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
      @errors[attribute.to_s] << msg
    end

    def add_on_empty(attributes, msg = @@default_error_messages[:empty])
      for attr in [attributes].flatten
        value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
        is_empty = value.respond_to?("empty?") ? value.empty? : false
        add(attr, msg) unless !value.nil? && !is_empty
      end
    end

  end

  module Validations
    def self.included(base) # :nodoc:
      base.extend ClassMethods
      base.class_eval do
        alias_method_chain :save, :validation
        alias_method_chain :save!, :validation
        alias_method_chain :update_attribute, :validation_skipping
      end
    end

    module ClassMethods

      def validate(*methods, &block)
        methods << block if block_given?
        write_inheritable_set(:validate, methods)
      end

      def validate_on_create(*methods, &block)
        methods << block if block_given?
        write_inheritable_set(:validate_on_create, methods)
      end

      def validate_on_update(*methods, &block)
        methods << block if block_given?
        write_inheritable_set(:validate_on_update, methods)
      end

      def validates_each(*attrs)
        options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {}
        attrs   = attrs.flatten

        # Declare the validation.
        send(validation_method(options[:on] || :save)) do |record|
          # Don't validate when there is an :if condition and that condition is false
          unless options[:if] && !evaluate_condition(options[:if], record)
            attrs.each do |attr|
              value = record.send(attr)
              next if value.nil? && options[:allow_nil]
              yield record, attr, value
            end
          end
        end
      end

      def validates_confirmation_of(*attr_names)
        configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save }
        configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

        attr_accessor *(attr_names.map { |n| "#{n}_confirmation" })

        validates_each(attr_names, configuration) do |record, attr_name, value|
          record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
        end
      end

      def validates_presence_of(*attr_names)
        configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save }
        configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

        # can't use validates_each here, because it cannot cope with nonexistent attributes,
        # while errors.add_on_empty can
        attr_names.each do |attr_name|
          send(validation_method(configuration[:on])) do |record|
            unless configuration[:if] and not evaluate_condition(configuration[:if], record)
              record.errors.add_on_blank(attr_name,configuration[:message])
            end
          end
        end
      end

      def validates_length_of(*attrs)
        # Merge given options with defaults.
        options = {
          :too_long     => ActiveRecord::Errors.default_error_messages[:too_long],
          :too_short    => ActiveRecord::Errors.default_error_messages[:too_short],
          :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length]
        }.merge(DEFAULT_VALIDATION_OPTIONS)
        options.update(attrs.pop.symbolize_keys) if attrs.last.is_a?(Hash)

        # Ensure that one and only one range option is specified.
        range_options = ALL_RANGE_OPTIONS & options.keys
        case range_options.size
          when 0
            raise ArgumentError, 'Range unspecified.  Specify the :within, :maximum, :minimum, or :is option.'
          when 1
            # Valid number of options; do nothing.
          else
            raise ArgumentError, 'Too many range options specified.  Choose only one.'
        end

        # Get range option and value.
        option = range_options.first
        option_value = options[range_options.first]

        case option
          when :within, :in
            raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)

            too_short = options[:too_short] % option_value.begin
            too_long  = options[:too_long]  % option_value.end

            validates_each(attrs, options) do |record, attr, value|
              if value.nil? or value.split(//).size < option_value.begin
                record.errors.add(attr, too_short)
              elsif value.split(//).size > option_value.end
                record.errors.add(attr, too_long)
              end
            end
          when :is, :minimum, :maximum
            raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0

            # Declare different validations per option.
            validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
            message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }

            message = (options[:message] || options[message_options[option]]) % option_value

            validates_each(attrs, options) do |record, attr, value|
              if value.kind_of?(String)
                record.errors.add(attr, message) unless !value.nil? and value.split(//).size.method(validity_checks[option])[option_value]
              else
                record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
              end
            end
        end
      end

      alias_method :validates_size_of, :validates_length_of

      def validates_uniqueness_of(*attr_names)
        configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true }
        configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

        validates_each(attr_names,configuration) do |record, attr_name, value|
          if value.nil? || (configuration[:case_sensitive] || !columns_hash[attr_name.to_s].text?)
            condition_sql = "#{record.class.table_name}.#{attr_name} #{attribute_condition(value)}"
            condition_params = [value]
          else
            condition_sql = "LOWER(#{record.class.table_name}.#{attr_name}) #{attribute_condition(value)}"
            condition_params = [value.downcase]
          end
          if scope = configuration[:scope]
            Array(scope).map do |scope_item|
              scope_value = record.send(scope_item)
              condition_sql << " AND #{record.class.table_name}.#{scope_item} #{attribute_condition(scope_value)}"
              condition_params << scope_value
            end
          end
          unless record.new_record?
            condition_sql << " AND #{record.class.table_name}.#{record.class.primary_key} <> ?"
            condition_params << record.send(:id)
          end
          if record.class.find(:first, :conditions => [condition_sql, *condition_params])
            record.errors.add(attr_name, configuration[:message])
          end
        end
      end

      def validates_format_of(*attr_names)
        configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
        configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

        raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)

        validates_each(attr_names, configuration) do |record, attr_name, value|
          record.errors.add(attr_name, configuration[:message]) unless value.to_s =~ configuration[:with]
        end
      end

      def validates_numericality_of(*attr_names)
        configuration = { :message => ActiveRecord::Errors.default_error_messages[:not_a_number], :on => :save,
                           :only_integer => false, :allow_nil => false }
        configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

        if configuration[:only_integer]
          validates_each(attr_names,configuration) do |record, attr_name,value|
            record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_before_type_cast").to_s =~ /\A[+-]?\d+\Z/
          end
        else
          validates_each(attr_names,configuration) do |record, attr_name,value|
           next if configuration[:allow_nil] and record.send("#{attr_name}_before_type_cast").nil?
            begin
              Kernel.Float(record.send("#{attr_name}_before_type_cast").to_s)
            rescue ArgumentError, TypeError
              record.errors.add(attr_name, configuration[:message])
            end
          end
        end
      end
    end

    def valid?
      errors.clear

      run_validations(:validate)
      validate

      if new_record?
        run_validations(:validate_on_create)
        validate_on_create
      else
        run_validations(:validate_on_update)
        validate_on_update
      end

      errors.empty?
    end

    def errors
      @errors ||= Errors.new(self)
    end
  end
end      

从上述代码我们可以看出以下几点:
1,save、save!方法会使用validation,而update_attribute则skip validation
2,record.errors方法得到该record经过validate后的所有errors
3,save(false)会skip validation

overwriting Base#validate的例子:
class Person < ActiveRecord::Base
  protected
    def validate
      errors.add_on_empty %w( first_name last_name )
      errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
    end

    def validate_on_create # is only run the first time a new object is saved
      unless valid_discount?(membership_discount)
        errors.add("membership_discount", "has expired")
      end
    end

    def validate_on_update
      errors.add_to_base("No changes have occurred") if unchanged_attributes?
    end
end


view中:
<% unless @post.errors.empty? %>
  The post couldn't be saved due to these errors:
  <ul>
  <% @post.errors.each_full do |message| %>
    <li><%= message %></li>
  <% end %>
  </ul>
<% end %>

或者
<%= error_messages_for 'post' %>


看了validations的代码,我们甚至可以写自己的Validation关键字:
module ActiveRecord
  module Validations
    module ClassMethods
      def validates_oak_id(*attr_names)
        configuration = { :message => "Invalid Oak ID" }
        configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

        validates_each (attr_names, configuration) do |record, attr_name, value|
          record.errors.add(attr_name, configuration[:message]) unless FAUtils.valid_oakid?(value)
        end
      end
    end
  end
end

这个例子使用FAUtils库来验证oakid的合法性,一切都很简单吧?
分享到:
评论
3 楼 axgle 2007-06-22  
能不能使用ActiveRecord::Validations with out ActiveRecord::Base?
可以的.
require 'active_record'

module Emptyar
  def save; end     
  def save!;end  
  def update_attribute; end
  def new_record?;end
  def self.included(model)
    model.send(:include,ActiveRecord::Validations)
  end  
end

class Post
  include Emptyar 
  attr_accessor :title,:body
  validates_presence_of :title 
  validates_size_of :body,:in=>(1..1000)
end

a=Post.new
puts a.valid?
puts a.errors.on("title")
puts a.errors.on("body")

a.title = "tester"
puts a.valid?
puts a.errors.on("title")

a.body = "tester content"
puts a.valid?
puts a.errors.on("body")
2 楼 hideto 2007-06-21  
1,validations的调用不是在action层调用的,而是model层
2,上面说到了save和save!时会自动调用validations
3,手动调用Model的validations的方法是@model.validate
4,手动调用Model的某个attr的validation的方法可以是
class Person < ActiveRecord::Base
  validates_each :first_name, :last_name do |record, attr, value|
    record.errors.add attr, 'starts with z.' if value[0] == ?z
  end
end

源码中有Example,写的很清除了
Validations ClassMethods
1 楼 crazysoul 2007-06-21  
那么在action里要如何调用某些validations呢?
比如要做用户更改密码的操作,已经在model里设置了
  attr_accessor :password_confirmation
  validates_confirmation_of :password

相关推荐

    rails_async_migrations:对ActiveRecord :: Migration的异步支持

    RailsAsyncMigrations ActiveRecord::Migration扩展程序以一种简单直接的方式使您的迁移异步。动机创建该库的目的是为了帮助在技术水平上难以扩展的小型公司。 小型项目不需要异步迁移队列,大公司在遇到扩展问题时...

    Rails的精简版本Rails::API.zip

    Rails::API 是 Rails 的精简版本,针对不需要使用完整 Rails 功能的开发者。 Rails::API 移除了 ActionView 和其他一些渲染功能,不关心Web前端的开发者可更容易、快速地开发应用程序,因此运行速度比正常的 Rails ...

    userstamp, 这个 Rails 插件扩展ActiveRecord.zip

    userstamp, 这个 Rails 插件扩展ActiveRecord Userstamp插件( v-2.0 )概述Userstamp插件扩展了 ActiveRecord::Base,以添加对'创建者','更新程序'和'deleter'属性的自动更新。 它是基于 ActiveRecord::Timesta

    to_xls-rails:将Rails ActiveRecord或Mongid数据导出到Excel文件

    数组元素支持对象:ActiveRecord,Mongid,哈希。 在您的Gemfile中: gem 'to_xls-rails' # Last officially released gem # gem "to_xls-rails", :git =&gt; "git://github....

    ROR绿色最新环境(2013/3/10)

    Rails::Rack::Logger ActionDispatch::ShowExceptions ActionDispatch::DebugExceptions ActionDispatch::RemoteIp ActionDispatch::Reloader ActionDispatch::Callbacks ActiveRecord::ConnectionAdapters::...

    ActiveRecord简单实例_activerecord.zip

    在Ruby on Rails框架中,ActiveRecord是一个至关重要的组件,它负责模型(Model)与数据库之间的交互。本实例将深入探讨ActiveRecord的基本用法,帮助理解如何在实际开发中有效地运用这个强大的工具。 首先,让我们...

    stateful_enum:基于ActiveRecord :: Enum构建的非常简单的状态机插件

    stateful_enum是建立在ActiveRecord的内置ActiveRecord :: Enum之上的状态机gem。 安装 将此行添加到您的Rails应用程序的Gemfile中: gem 'stateful_enum' 和捆绑。 动机 您不需要抽象 stateful_enum取决于...

    ActiveRecord-Without-Rails:只是在没有Rails的情况下使用ActiveRecord迁移的简单示例

    没有Rails的ActiveRecord 只是在没有Rails的情况下使用ActiveRecord迁移的简单示例您可以执行的任务: rake db:create rake db:migrate rake db:dropRails 5+的注意事项请注意,即使使用Rails 5,您也需要rake db:...

    枚举:具有I18n和ActiveRecordMongoid支持的枚举属性

    Rails 5.2+用法基本: class User extend Enumerize enumerize :sex , in : [ :male , :female ]end 请注意,枚举值仅是标识符,因此,如果要使用多字等值,则应使用I18n功能。 ActiveRecord: class CreateUsers &...

    Ruby on Rails入门经典代码

    Ruby on Rails,简称Rails,是基于Ruby语言的一个开源Web应用程序框架,它遵循MVC(Model-View-Controller)架构模式,旨在使Web开发过程更加高效、简洁。本压缩包中的"Ruby on Rails入门经典代码"提供了新手学习...

    Ruby on Rails中的ActiveRecord编程指南

    ### Ruby on Rails中的ActiveRecord编程指南 #### 一、引言 在Ruby on Rails框架中,ActiveRecord是一种用于实现数据库抽象层的对象关系映射(ORM)工具。它为开发人员提供了一种简单而强大的方式来处理数据库记录...

    Rails项目源代码

    这个Rails项目提供了学习和研究Web开发的机会,特别是对于Ruby on Rails新手,可以通过阅读和理解源代码来提升技能,了解实际应用中Rails的用法。同时,对于有经验的开发者,这个项目也可以作为一个起点,进行二次...

    paranoid2:Rails 4 的偏执模型

    Rails 4 定义了ActiveRecord::Base#destroy! 所以Paranoid2 gem 使用force: true arg 来强制销毁。 安装 将此行添加到应用程序的 Gemfile 中: gem 'paranoid2' 然后执行: $ bundle 用法 将deleted_at: ...

    hashid-rails:在Rails应用程序ActiveRecord模型中使用Hashids(http:hashids.orgruby)

    在要启用哈希值的ActiveRecord模型中包括Hashid Rails。 class Model &lt; ActiveRecord :: Base include Hashid :: Rails end 继续使用Model#find输入hashid或常规的'ol id。 @person = Person . find ( params...

    rails-exporter-源码.rar

    《Rails Exporter 源码解析》 Rails Exporter 是一个用于 Rails 应用程序的开源工具,主要用于数据导出功能。源码分析将帮助我们深入理解其内部工作原理,以便更好地利用它来优化我们的应用。 一、Rails 框架基础 ...

    workflow-activerecord:工作流库的 ActiveRecordRails 集成

    工作流库的 ActiveRecord/Rails 集成 工作流活动记录的主要+次要版本基于最旧的兼容 ActiveRecord API。 要在 Rails/ActiveRecord 4.1、4.2、5.0、5.1、5.2、6.0、6.1 中使用 ,请使用: gem 'workflow-...

    flowdock-rails:用于 ActiveRecord 模型的 Flowdock 通知程序

    Flowdock::Rails 这个 gem 添加了一个类方法来向特定流发送通知,以在启用的资源上创建和更新事件安装将此行添加到应用程序的 Gemfile 中: gem 'flowdock-rails'然后执行: $ bundle或者自己安装: $ gem install ...

    Ruby on Rails入门例子

    - **ActiveRecord查询接口(Query Interface)**:ActiveRecord提供了丰富的查询API,如`User.find(1)`, `Post.where(title: 'Hello')`,用于从数据库检索数据。 - ** erb语法**:在视图文件中,我们可以使用erb...

    Rails上的API:使用Rails构建REST APIAPIs on Rails: Building REST APIs with Rails

    1. **快速开发**:Rails内置了许多实用的功能和库,如ActiveRecord ORM、MVC架构等,这些都能够极大地加快开发进度。 2. **代码简洁**:Rails遵循“约定优于配置”的原则,这意味着开发者无需编写大量重复代码就能...

    Apress Pro ActiveRecord Databases with Ruby and Rails.pdf

    1. **自动化的表映射**:ActiveRecord可以根据类名自动推断出对应的数据库表名,并根据类属性推断出表中的列。 2. **数据验证**:提供了丰富的验证规则,如唯一性验证、存在性验证等,确保数据的完整性和一致性。 3....

Global site tag (gtag.js) - Google Analytics