Upgrade to Rails 2.2.0
As a side benefit, fix an (non-user-visible) bug in display_s5(). Also fixed a bug where removing orphaned pages did not expire cached summary pages.
This commit is contained in:
parent
39348c65c2
commit
7600aef48b
827 changed files with 123652 additions and 11027 deletions
21
vendor/rails/activerecord/lib/active_record.rb
vendored
21
vendor/rails/activerecord/lib/active_record.rb
vendored
|
@ -21,17 +21,14 @@
|
|||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#++
|
||||
|
||||
$:.unshift(File.dirname(__FILE__)) unless
|
||||
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
||||
|
||||
active_support_path = File.dirname(__FILE__) + "/../../activesupport/lib"
|
||||
if File.exist?(active_support_path)
|
||||
$:.unshift active_support_path
|
||||
require 'active_support'
|
||||
else
|
||||
require 'rubygems'
|
||||
gem 'activesupport'
|
||||
begin
|
||||
require 'active_support'
|
||||
rescue LoadError
|
||||
activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
|
||||
if File.directory?(activesupport_path)
|
||||
$:.unshift activesupport_path
|
||||
require 'active_support'
|
||||
end
|
||||
end
|
||||
|
||||
require 'active_record/base'
|
||||
|
@ -54,6 +51,7 @@ require 'active_record/calculations'
|
|||
require 'active_record/serialization'
|
||||
require 'active_record/attribute_methods'
|
||||
require 'active_record/dirty'
|
||||
require 'active_record/dynamic_finder_match'
|
||||
|
||||
ActiveRecord::Base.class_eval do
|
||||
extend ActiveRecord::QueryCache
|
||||
|
@ -78,3 +76,6 @@ end
|
|||
require 'active_record/connection_adapters/abstract_adapter'
|
||||
|
||||
require 'active_record/schema_dumper'
|
||||
|
||||
require 'active_record/i18n_interpolation_deprecation'
|
||||
I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en-US.yml'
|
||||
|
|
|
@ -10,10 +10,10 @@ module ActiveRecord
|
|||
end unless self.new_record?
|
||||
end
|
||||
|
||||
# Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
|
||||
# Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
|
||||
# as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is]
|
||||
# composed of [an] address". Each call to the macro adds a description of how the value objects are created from the
|
||||
# attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object)
|
||||
# composed of [an] address". Each call to the macro adds a description of how the value objects are created from the
|
||||
# attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object)
|
||||
# and how it can be turned back into attributes (when the entity is saved to the database). Example:
|
||||
#
|
||||
# class Customer < ActiveRecord::Base
|
||||
|
@ -30,10 +30,10 @@ module ActiveRecord
|
|||
# class Money
|
||||
# include Comparable
|
||||
# attr_reader :amount, :currency
|
||||
# EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
|
||||
#
|
||||
# def initialize(amount, currency = "USD")
|
||||
# @amount, @currency = amount, currency
|
||||
# EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
|
||||
#
|
||||
# def initialize(amount, currency = "USD")
|
||||
# @amount, @currency = amount, currency
|
||||
# end
|
||||
#
|
||||
# def exchange_to(other_currency)
|
||||
|
@ -56,26 +56,26 @@ module ActiveRecord
|
|||
#
|
||||
# class Address
|
||||
# attr_reader :street, :city
|
||||
# def initialize(street, city)
|
||||
# @street, @city = street, city
|
||||
# def initialize(street, city)
|
||||
# @street, @city = street, city
|
||||
# end
|
||||
#
|
||||
# def close_to?(other_address)
|
||||
# city == other_address.city
|
||||
# def close_to?(other_address)
|
||||
# city == other_address.city
|
||||
# end
|
||||
#
|
||||
# def ==(other_address)
|
||||
# city == other_address.city && street == other_address.street
|
||||
# end
|
||||
# end
|
||||
#
|
||||
#
|
||||
# Now it's possible to access attributes from the database through the value objects instead. If you choose to name the
|
||||
# composition the same as the attribute's name, it will be the only way to access that attribute. That's the case with our
|
||||
# +balance+ attribute. You interact with the value objects just like you would any other attribute, though:
|
||||
#
|
||||
# customer.balance = Money.new(20) # sets the Money value object and the attribute
|
||||
# customer.balance # => Money value object
|
||||
# customer.balance.exchanged_to("DKK") # => Money.new(120, "DKK")
|
||||
# customer.balance.exchange_to("DKK") # => Money.new(120, "DKK")
|
||||
# customer.balance > Money.new(10) # => true
|
||||
# customer.balance == Money.new(20) # => true
|
||||
# customer.balance < Money.new(5) # => false
|
||||
|
@ -87,8 +87,8 @@ module ActiveRecord
|
|||
# customer.address_city = "Copenhagen"
|
||||
# customer.address # => Address.new("Hyancintvej", "Copenhagen")
|
||||
# customer.address = Address.new("May Street", "Chicago")
|
||||
# customer.address_street # => "May Street"
|
||||
# customer.address_city # => "Chicago"
|
||||
# customer.address_street # => "May Street"
|
||||
# customer.address_city # => "Chicago"
|
||||
#
|
||||
# == Writing value objects
|
||||
#
|
||||
|
@ -99,16 +99,55 @@ module ActiveRecord
|
|||
# relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects.
|
||||
#
|
||||
# It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after
|
||||
# creation. Create a new Money object with the new value instead. This is exemplified by the Money#exchanged_to method that
|
||||
# creation. Create a new Money object with the new value instead. This is exemplified by the Money#exchange_to method that
|
||||
# returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
|
||||
# changed through means other than the writer method.
|
||||
#
|
||||
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
|
||||
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
|
||||
# change it afterwards will result in a ActiveSupport::FrozenObjectError.
|
||||
#
|
||||
#
|
||||
# Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects
|
||||
# immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
|
||||
#
|
||||
# == Custom constructors and converters
|
||||
#
|
||||
# By default value objects are initialized by calling the <tt>new</tt> constructor of the value class passing each of the
|
||||
# mapped attributes, in the order specified by the <tt>:mapping</tt> option, as arguments. If the value class doesn't support
|
||||
# this convention then +composed_of+ allows a custom constructor to be specified.
|
||||
#
|
||||
# When a new value is assigned to the value object the default assumption is that the new value is an instance of the value
|
||||
# class. Specifying a custom converter allows the new value to be automatically converted to an instance of value class if
|
||||
# necessary.
|
||||
#
|
||||
# For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be aggregated using the
|
||||
# NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor for the value class is called +create+ and it
|
||||
# expects a CIDR address string as a parameter. New values can be assigned to the value object using either another
|
||||
# NetAddr::CIDR object, a string or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to
|
||||
# meet these requirements:
|
||||
#
|
||||
# class NetworkResource < ActiveRecord::Base
|
||||
# composed_of :cidr,
|
||||
# :class_name => 'NetAddr::CIDR',
|
||||
# :mapping => [ %w(network_address network), %w(cidr_range bits) ],
|
||||
# :allow_nil => true,
|
||||
# :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
|
||||
# :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
|
||||
# end
|
||||
#
|
||||
# # This calls the :constructor
|
||||
# network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24)
|
||||
#
|
||||
# # These assignments will both use the :converter
|
||||
# network_resource.cidr = [ '192.168.2.1', 8 ]
|
||||
# network_resource.cidr = '192.168.0.1/24'
|
||||
#
|
||||
# # This assignment won't use the :converter as the value is already an instance of the value class
|
||||
# network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')
|
||||
#
|
||||
# # Saving and then reloading will use the :constructor on reload
|
||||
# network_resource.save
|
||||
# network_resource.reload
|
||||
#
|
||||
# == Finding records by a value object
|
||||
#
|
||||
# Once a +composed_of+ relationship is specified for a model, records can be loaded from the database by specifying an instance
|
||||
|
@ -122,47 +161,71 @@ module ActiveRecord
|
|||
# <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
|
||||
#
|
||||
# Options are:
|
||||
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred
|
||||
# * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name can't be inferred
|
||||
# from the part id. So <tt>composed_of :address</tt> will by default be linked to the Address class, but
|
||||
# if the real class name is CompanyAddress, you'll have to specify it with this option.
|
||||
# * <tt>:mapping</tt> - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name
|
||||
# to a constructor parameter on the value class.
|
||||
# * <tt>:allow_nil</tt> - specifies that the aggregate object will not be instantiated when all mapped
|
||||
# attributes are +nil+. Setting the aggregate class to +nil+ has the effect of writing +nil+ to all mapped attributes.
|
||||
# * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value object. Each mapping
|
||||
# is represented as an array where the first item is the name of the entity attribute and the second item is the
|
||||
# name the attribute in the value object. The order in which mappings are defined determine the order in which
|
||||
# attributes are sent to the value class constructor.
|
||||
# * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
|
||||
# attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all mapped attributes.
|
||||
# This defaults to +false+.
|
||||
#
|
||||
# An optional block can be passed to convert the argument that is passed to the writer method into an instance of
|
||||
# <tt>:class_name</tt>. The block will only be called if the argument is not already an instance of <tt>:class_name</tt>.
|
||||
# * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that is called to
|
||||
# initialize the value object. The constructor is passed all of the mapped attributes, in the order that they
|
||||
# are defined in the <tt>:mapping option</tt>, as arguments and uses them to instantiate a <tt>:class_name</tt> object.
|
||||
# The default is <tt>:new</tt>.
|
||||
# * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt> or a Proc that is
|
||||
# called when a new value is assigned to the value object. The converter is passed the single value that is used
|
||||
# in the assignment and is only called if the new value is not an instance of <tt>:class_name</tt>.
|
||||
#
|
||||
# Option examples:
|
||||
# composed_of :temperature, :mapping => %w(reading celsius)
|
||||
# composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) {|balance| balance.to_money }
|
||||
# composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money }
|
||||
# composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
|
||||
# composed_of :gps_location
|
||||
# composed_of :gps_location, :allow_nil => true
|
||||
# composed_of :ip_address,
|
||||
# :class_name => 'IPAddr',
|
||||
# :mapping => %w(ip to_i),
|
||||
# :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
|
||||
# :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
|
||||
#
|
||||
def composed_of(part_id, options = {}, &block)
|
||||
options.assert_valid_keys(:class_name, :mapping, :allow_nil)
|
||||
options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
|
||||
|
||||
name = part_id.id2name
|
||||
class_name = options[:class_name] || name.camelize
|
||||
mapping = options[:mapping] || [ name, name ]
|
||||
class_name = options[:class_name] || name.camelize
|
||||
mapping = options[:mapping] || [ name, name ]
|
||||
mapping = [ mapping ] unless mapping.first.is_a?(Array)
|
||||
allow_nil = options[:allow_nil] || false
|
||||
allow_nil = options[:allow_nil] || false
|
||||
constructor = options[:constructor] || :new
|
||||
converter = options[:converter] || block
|
||||
|
||||
ActiveSupport::Deprecation.warn('The conversion block has been deprecated, use the :converter option instead.', caller) if block_given?
|
||||
|
||||
reader_method(name, class_name, mapping, allow_nil, constructor)
|
||||
writer_method(name, class_name, mapping, allow_nil, converter)
|
||||
|
||||
reader_method(name, class_name, mapping, allow_nil)
|
||||
writer_method(name, class_name, mapping, allow_nil, block)
|
||||
|
||||
create_reflection(:composed_of, part_id, options, self)
|
||||
end
|
||||
|
||||
private
|
||||
def reader_method(name, class_name, mapping, allow_nil)
|
||||
def reader_method(name, class_name, mapping, allow_nil, constructor)
|
||||
module_eval do
|
||||
define_method(name) do |*args|
|
||||
force_reload = args.first || false
|
||||
if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
|
||||
instance_variable_set("@#{name}", class_name.constantize.new(*mapping.collect {|pair| read_attribute(pair.first)}))
|
||||
attrs = mapping.collect {|pair| read_attribute(pair.first)}
|
||||
object = case constructor
|
||||
when Symbol
|
||||
class_name.constantize.send(constructor, *attrs)
|
||||
when Proc, Method
|
||||
constructor.call(*attrs)
|
||||
else
|
||||
raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.'
|
||||
end
|
||||
instance_variable_set("@#{name}", object)
|
||||
end
|
||||
instance_variable_get("@#{name}")
|
||||
end
|
||||
|
@ -170,14 +233,23 @@ module ActiveRecord
|
|||
|
||||
end
|
||||
|
||||
def writer_method(name, class_name, mapping, allow_nil, conversion)
|
||||
def writer_method(name, class_name, mapping, allow_nil, converter)
|
||||
module_eval do
|
||||
define_method("#{name}=") do |part|
|
||||
if part.nil? && allow_nil
|
||||
mapping.each { |pair| self[pair.first] = nil }
|
||||
instance_variable_set("@#{name}", nil)
|
||||
else
|
||||
part = conversion.call(part) unless part.is_a?(class_name.constantize) || conversion.nil?
|
||||
unless part.is_a?(class_name.constantize) || converter.nil?
|
||||
part = case converter
|
||||
when Symbol
|
||||
class_name.constantize.send(converter, part)
|
||||
when Proc, Method
|
||||
converter.call(part)
|
||||
else
|
||||
raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.'
|
||||
end
|
||||
end
|
||||
mapping.each { |pair| self[pair.first] = part.send(pair.last) }
|
||||
instance_variable_set("@#{name}", part.freeze)
|
||||
end
|
||||
|
|
|
@ -1,14 +1,88 @@
|
|||
module ActiveRecord
|
||||
# See ActiveRecord::AssociationPreload::ClassMethods for documentation.
|
||||
module AssociationPreload #:nodoc:
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
# Implements the details of eager loading of ActiveRecord associations.
|
||||
# Application developers should not use this module directly.
|
||||
#
|
||||
# ActiveRecord::Base is extended with this module. The source code in
|
||||
# ActiveRecord::Base references methods defined in this module.
|
||||
#
|
||||
# Note that 'eager loading' and 'preloading' are actually the same thing.
|
||||
# However, there are two different eager loading strategies.
|
||||
#
|
||||
# The first one is by using table joins. This was only strategy available
|
||||
# prior to Rails 2.1. Suppose that you have an Author model with columns
|
||||
# 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
|
||||
# this strategy, ActiveRecord would try to retrieve all data for an author
|
||||
# and all of its books via a single query:
|
||||
#
|
||||
# SELECT * FROM authors
|
||||
# LEFT OUTER JOIN books ON authors.id = books.id
|
||||
# WHERE authors.name = 'Ken Akamatsu'
|
||||
#
|
||||
# However, this could result in many rows that contain redundant data. After
|
||||
# having received the first row, we already have enough data to instantiate
|
||||
# the Author object. In all subsequent rows, only the data for the joined
|
||||
# 'books' table is useful; the joined 'authors' data is just redundant, and
|
||||
# processing this redundant data takes memory and CPU time. The problem
|
||||
# quickly becomes worse and worse as the level of eager loading increases
|
||||
# (i.e. if ActiveRecord is to eager load the associations' assocations as
|
||||
# well).
|
||||
#
|
||||
# The second strategy is to use multiple database queries, one for each
|
||||
# level of association. Since Rails 2.1, this is the default strategy. In
|
||||
# situations where a table join is necessary (e.g. when the +:conditions+
|
||||
# option references an association's column), it will fallback to the table
|
||||
# join strategy.
|
||||
#
|
||||
# See also ActiveRecord::Associations::ClassMethods, which explains eager
|
||||
# loading in a more high-level (application developer-friendly) manner.
|
||||
module ClassMethods
|
||||
|
||||
# Loads the named associations for the activerecord record (or records) given
|
||||
# preload_options is passed only one level deep: don't pass to the child associations when associations is a Hash
|
||||
protected
|
||||
|
||||
# Eager loads the named associations for the given ActiveRecord record(s).
|
||||
#
|
||||
# In this description, 'association name' shall refer to the name passed
|
||||
# to an association creation method. For example, a model that specifies
|
||||
# <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
|
||||
# names +:author+ and +:buyers+.
|
||||
#
|
||||
# == Parameters
|
||||
# +records+ is an array of ActiveRecord::Base. This array needs not be flat,
|
||||
# i.e. +records+ itself may also contain arrays of records. In any case,
|
||||
# +preload_associations+ will preload the associations all records by
|
||||
# flattening +records+.
|
||||
#
|
||||
# +associations+ specifies one or more associations that you want to
|
||||
# preload. It may be:
|
||||
# - a Symbol or a String which specifies a single association name. For
|
||||
# example, specifiying +:books+ allows this method to preload all books
|
||||
# for an Author.
|
||||
# - an Array which specifies multiple association names. This array
|
||||
# is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
|
||||
# allows this method to preload an author's avatar as well as all of his
|
||||
# books.
|
||||
# - a Hash which specifies multiple association names, as well as
|
||||
# association names for the to-be-preloaded association objects. For
|
||||
# example, specifying <tt>{ :author => :avatar }</tt> will preload a
|
||||
# book's author, as well as that author's avatar.
|
||||
#
|
||||
# +:associations+ has the same format as the +:include+ option for
|
||||
# <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
|
||||
#
|
||||
# :books
|
||||
# [ :books, :author ]
|
||||
# { :author => :avatar }
|
||||
# [ :books, { :author => :avatar } ]
|
||||
#
|
||||
# +preload_options+ contains options that will be passed to ActiveRecord#find
|
||||
# (which is called under the hood for preloading records). But it is passed
|
||||
# only one level deep in the +associations+ argument, i.e. it's not passed
|
||||
# to the child associations when +associations+ is a Hash.
|
||||
def preload_associations(records, associations, preload_options={})
|
||||
records = [records].flatten.compact.uniq
|
||||
return if records.empty?
|
||||
|
@ -30,13 +104,19 @@ module ActiveRecord
|
|||
|
||||
private
|
||||
|
||||
# Preloads a specific named association for the given records. This is
|
||||
# called by +preload_associations+ as its base case.
|
||||
def preload_one_association(records, association, preload_options={})
|
||||
class_to_reflection = {}
|
||||
# Not all records have the same class, so group then preload
|
||||
# group on the reflection itself so that if various subclass share the same association then we do not split them
|
||||
# unncessarily
|
||||
# unnecessarily
|
||||
records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records|
|
||||
raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
|
||||
|
||||
# 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus,
|
||||
# the following could call 'preload_belongs_to_association',
|
||||
# 'preload_has_many_association', etc.
|
||||
send("preload_#{reflection.macro}_association", records, reflection, preload_options)
|
||||
end
|
||||
end
|
||||
|
@ -77,12 +157,17 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def construct_id_map(records)
|
||||
# Given a collection of ActiveRecord objects, constructs a Hash which maps
|
||||
# the objects' IDs to the relevant objects. Returns a 2-tuple
|
||||
# <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash,
|
||||
# and +ids+ is an Array of record IDs.
|
||||
def construct_id_map(records, primary_key=nil)
|
||||
id_to_record_map = {}
|
||||
ids = []
|
||||
records.each do |record|
|
||||
ids << record.id
|
||||
mapped_records = (id_to_record_map[record.id.to_s] ||= [])
|
||||
primary_key ||= record.class.primary_key
|
||||
ids << record[primary_key]
|
||||
mapped_records = (id_to_record_map[ids.last.to_s] ||= [])
|
||||
mapped_records << record
|
||||
end
|
||||
ids.uniq!
|
||||
|
@ -95,8 +180,8 @@ module ActiveRecord
|
|||
records.each {|record| record.send(reflection.name).loaded}
|
||||
options = reflection.options
|
||||
|
||||
conditions = "t0.#{reflection.primary_key_name} IN (?)"
|
||||
conditions << append_conditions(options, preload_options)
|
||||
conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}"
|
||||
conditions << append_conditions(reflection, preload_options)
|
||||
|
||||
associated_records = reflection.klass.find(:all, :conditions => [conditions, ids],
|
||||
:include => options[:include],
|
||||
|
@ -108,6 +193,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def preload_has_one_association(records, reflection, preload_options={})
|
||||
return if records.first.send("loaded_#{reflection.name}?")
|
||||
id_to_record_map, ids = construct_id_map(records)
|
||||
options = reflection.options
|
||||
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
|
||||
|
@ -129,23 +215,25 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def preload_has_many_association(records, reflection, preload_options={})
|
||||
id_to_record_map, ids = construct_id_map(records)
|
||||
records.each {|record| record.send(reflection.name).loaded}
|
||||
return if records.first.send(reflection.name).loaded?
|
||||
options = reflection.options
|
||||
|
||||
primary_key_name = reflection.through_reflection_primary_key_name
|
||||
id_to_record_map, ids = construct_id_map(records, primary_key_name)
|
||||
records.each {|record| record.send(reflection.name).loaded}
|
||||
|
||||
if options[:through]
|
||||
through_records = preload_through_records(records, reflection, options[:through])
|
||||
through_reflection = reflections[options[:through]]
|
||||
through_primary_key = through_reflection.primary_key_name
|
||||
unless through_records.empty?
|
||||
source = reflection.source_reflection.name
|
||||
#add conditions from reflection!
|
||||
through_records.first.class.preload_associations(through_records, source, reflection.options)
|
||||
through_records.first.class.preload_associations(through_records, source, options)
|
||||
through_records.each do |through_record|
|
||||
add_preloaded_records_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
|
||||
reflection.name, through_record.send(source))
|
||||
through_record_id = through_record[reflection.through_reflection_primary_key].to_s
|
||||
add_preloaded_records_to_collection(id_to_record_map[through_record_id], reflection.name, through_record.send(source))
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options),
|
||||
reflection.primary_key_name)
|
||||
|
@ -185,6 +273,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def preload_belongs_to_association(records, reflection, preload_options={})
|
||||
return if records.first.send("loaded_#{reflection.name}?")
|
||||
options = reflection.options
|
||||
primary_key_name = reflection.primary_key_name
|
||||
|
||||
|
@ -222,8 +311,6 @@ module ActiveRecord
|
|||
|
||||
table_name = klass.quoted_table_name
|
||||
primary_key = klass.primary_key
|
||||
conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} IN (?)"
|
||||
conditions << append_conditions(options, preload_options)
|
||||
column_type = klass.columns.detect{|c| c.name == primary_key}.type
|
||||
ids = id_map.keys.uniq.map do |id|
|
||||
if column_type == :integer
|
||||
|
@ -234,6 +321,8 @@ module ActiveRecord
|
|||
id
|
||||
end
|
||||
end
|
||||
conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}"
|
||||
conditions << append_conditions(reflection, preload_options)
|
||||
associated_records = klass.find(:all, :conditions => [conditions, ids],
|
||||
:include => options[:include],
|
||||
:select => options[:select],
|
||||
|
@ -248,13 +337,13 @@ module ActiveRecord
|
|||
table_name = reflection.klass.quoted_table_name
|
||||
|
||||
if interface = reflection.options[:as]
|
||||
conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'"
|
||||
conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'"
|
||||
else
|
||||
foreign_key = reflection.primary_key_name
|
||||
conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)"
|
||||
conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}"
|
||||
end
|
||||
|
||||
conditions << append_conditions(options, preload_options)
|
||||
conditions << append_conditions(reflection, preload_options)
|
||||
|
||||
reflection.klass.find(:all,
|
||||
:select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
|
||||
|
@ -270,13 +359,16 @@ module ActiveRecord
|
|||
instance_eval("%@#{sql.gsub('@', '\@')}@")
|
||||
end
|
||||
|
||||
def append_conditions(options, preload_options)
|
||||
def append_conditions(reflection, preload_options)
|
||||
sql = ""
|
||||
sql << " AND (#{interpolate_sql_for_preload(sanitize_sql(options[:conditions]))})" if options[:conditions]
|
||||
sql << " AND (#{interpolate_sql_for_preload(reflection.sanitized_conditions)})" if reflection.sanitized_conditions
|
||||
sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions]
|
||||
sql
|
||||
end
|
||||
|
||||
def in_or_equals_for_ids(ids)
|
||||
ids.size > 1 ? "IN (?)" : "= ?"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2,6 +2,19 @@ require 'set'
|
|||
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
# AssociationCollection is an abstract class that provides common stuff to
|
||||
# ease the implementation of association proxies that represent
|
||||
# collections. See the class hierarchy in AssociationProxy.
|
||||
#
|
||||
# You need to be careful with assumptions regarding the target: The proxy
|
||||
# does not fetch records from the database until it needs them, but new
|
||||
# ones created with +build+ are added to the target. So, the target may be
|
||||
# non-empty and still lack children waiting to be read from the database.
|
||||
# If you look directly to the database you cannot assume that's the entire
|
||||
# collection because new records may have beed added to the target, etc.
|
||||
#
|
||||
# If you need to work on all current children, new and existing records,
|
||||
# +load_target+ and the +loaded+ flag are your friends.
|
||||
class AssociationCollection < AssociationProxy #:nodoc:
|
||||
def initialize(owner, reflection)
|
||||
super
|
||||
|
@ -14,7 +27,7 @@ module ActiveRecord
|
|||
# If using a custom finder_sql, scan the entire collection.
|
||||
if @reflection.options[:finder_sql]
|
||||
expects_array = args.first.kind_of?(Array)
|
||||
ids = args.flatten.compact.uniq.map(&:to_i)
|
||||
ids = args.flatten.compact.uniq.map { |arg| arg.to_i }
|
||||
|
||||
if ids.size == 1
|
||||
id = ids.first
|
||||
|
@ -50,7 +63,7 @@ module ActiveRecord
|
|||
|
||||
# Fetches the first one using SQL if possible.
|
||||
def first(*args)
|
||||
if fetch_first_or_last_using_find? args
|
||||
if fetch_first_or_last_using_find?(args)
|
||||
find(:first, *args)
|
||||
else
|
||||
load_target unless loaded?
|
||||
|
@ -60,7 +73,7 @@ module ActiveRecord
|
|||
|
||||
# Fetches the last one using SQL if possible.
|
||||
def last(*args)
|
||||
if fetch_first_or_last_using_find? args
|
||||
if fetch_first_or_last_using_find?(args)
|
||||
find(:last, *args)
|
||||
else
|
||||
load_target unless loaded?
|
||||
|
@ -95,7 +108,7 @@ module ActiveRecord
|
|||
result = true
|
||||
load_target if @owner.new_record?
|
||||
|
||||
@owner.transaction do
|
||||
transaction do
|
||||
flatten_deeper(records).each do |record|
|
||||
raise_on_type_mismatch(record)
|
||||
add_record_to_target_with_callbacks(record) do |r|
|
||||
|
@ -110,6 +123,21 @@ module ActiveRecord
|
|||
alias_method :push, :<<
|
||||
alias_method :concat, :<<
|
||||
|
||||
# Starts a transaction in the association class's database connection.
|
||||
#
|
||||
# class Author < ActiveRecord::Base
|
||||
# has_many :books
|
||||
# end
|
||||
#
|
||||
# Author.find(:first).books.transaction do
|
||||
# # same effect as calling Book.transaction
|
||||
# end
|
||||
def transaction(*args)
|
||||
@reflection.klass.transaction(*args) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# Remove all records from this association
|
||||
def delete_all
|
||||
load_target
|
||||
|
@ -126,12 +154,47 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Remove +records+ from this association. Does not destroy +records+.
|
||||
# Count all records using SQL. If the +:counter_sql+ option is set for the association, it will
|
||||
# be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the
|
||||
# descendant's +construct_sql+ method will have set :counter_sql automatically.
|
||||
# Otherwise, construct options and pass them with scope to the target class's +count+.
|
||||
def count(*args)
|
||||
if @reflection.options[:counter_sql]
|
||||
@reflection.klass.count_by_sql(@counter_sql)
|
||||
else
|
||||
column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
|
||||
if @reflection.options[:uniq]
|
||||
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
|
||||
column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all
|
||||
options.merge!(:distinct => true)
|
||||
end
|
||||
|
||||
value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
|
||||
|
||||
limit = @reflection.options[:limit]
|
||||
offset = @reflection.options[:offset]
|
||||
|
||||
if limit || offset
|
||||
[ [value - offset.to_i, 0].max, limit.to_i ].min
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Removes +records+ from this association calling +before_remove+ and
|
||||
# +after_remove+ callbacks.
|
||||
#
|
||||
# This method is abstract in the sense that +delete_records+ has to be
|
||||
# provided by descendants. Note this method does not imply the records
|
||||
# are actually removed from the database, that depends precisely on
|
||||
# +delete_records+. They are in any case removed from the collection.
|
||||
def delete(*records)
|
||||
records = flatten_deeper(records)
|
||||
records.each { |record| raise_on_type_mismatch(record) }
|
||||
|
||||
@owner.transaction do
|
||||
transaction do
|
||||
records.each { |record| callback(:before_remove, record) }
|
||||
|
||||
old_records = records.reject {|r| r.new_record? }
|
||||
|
@ -158,7 +221,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def destroy_all
|
||||
@owner.transaction do
|
||||
transaction do
|
||||
each { |record| record.destroy }
|
||||
end
|
||||
|
||||
|
@ -183,12 +246,21 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
|
||||
# calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
|
||||
# and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
|
||||
# Returns the size of the collection by executing a SELECT COUNT(*)
|
||||
# query if the collection hasn't been loaded, and calling
|
||||
# <tt>collection.size</tt> if it has.
|
||||
#
|
||||
# If the collection has been already loaded +size+ and +length+ are
|
||||
# equivalent. If not and you are going to need the records anyway
|
||||
# +length+ will take one less query. Otherwise +size+ is more efficient.
|
||||
#
|
||||
# This method is abstract in the sense that it relies on
|
||||
# +count_records+, which is a method descendants have to provide.
|
||||
def size
|
||||
if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
|
||||
@target.size
|
||||
elsif !loaded? && @reflection.options[:group]
|
||||
load_target.size
|
||||
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
|
||||
unsaved_records = @target.select { |r| r.new_record? }
|
||||
unsaved_records.size + count_records
|
||||
|
@ -197,12 +269,18 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
|
||||
# whether the collection is empty, use collection.length.zero? instead of collection.empty?
|
||||
# Returns the size of the collection calling +size+ on the target.
|
||||
#
|
||||
# If the collection has been already loaded +length+ and +size+ are
|
||||
# equivalent. If not and you are going to need the records anyway this
|
||||
# method will take one less query. Otherwise +size+ is more efficient.
|
||||
def length
|
||||
load_target.size
|
||||
end
|
||||
|
||||
# Equivalent to <tt>collection.size.zero?</tt>. If the collection has
|
||||
# not been already loaded and you are going to fetch the records anyway
|
||||
# it is better to check <tt>collection.length.zero?</tt>.
|
||||
def empty?
|
||||
size.zero?
|
||||
end
|
||||
|
@ -235,7 +313,7 @@ module ActiveRecord
|
|||
other = other_array.size < 100 ? other_array : other_array.to_set
|
||||
current = @target.size < 100 ? @target : @target.to_set
|
||||
|
||||
@owner.transaction do
|
||||
transaction do
|
||||
delete(@target.select { |v| !other.include?(v) })
|
||||
concat(other_array.select { |v| !current.include?(v) })
|
||||
end
|
||||
|
@ -248,6 +326,10 @@ module ActiveRecord
|
|||
exists?(record)
|
||||
end
|
||||
|
||||
def proxy_respond_to?(method, include_private = false)
|
||||
super || @reflection.klass.respond_to?(method, include_private)
|
||||
end
|
||||
|
||||
protected
|
||||
def construct_find_options!(options)
|
||||
end
|
||||
|
@ -316,7 +398,9 @@ module ActiveRecord
|
|||
def create_record(attrs)
|
||||
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
||||
ensure_owner_is_not_new
|
||||
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.new(attrs) }
|
||||
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
|
||||
@reflection.build_association(attrs)
|
||||
end
|
||||
if block_given?
|
||||
add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
|
||||
else
|
||||
|
@ -326,7 +410,7 @@ module ActiveRecord
|
|||
|
||||
def build_record(attrs)
|
||||
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
||||
record = @reflection.klass.new(attrs)
|
||||
record = @reflection.build_association(attrs)
|
||||
if block_given?
|
||||
add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
|
||||
else
|
||||
|
@ -361,7 +445,8 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def fetch_first_or_last_using_find?(args)
|
||||
args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] || !@target.blank? || args.first.kind_of?(Integer))
|
||||
args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] ||
|
||||
@target.any? { |record| record.new_record? } || args.first.kind_of?(Integer))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -39,7 +39,7 @@ module ActiveRecord
|
|||
# though the object behind <tt>blog.posts</tt> is not an Array, but an
|
||||
# ActiveRecord::Associations::HasManyAssociation.
|
||||
#
|
||||
# The <tt>@target</tt> object is not loaded until needed. For example,
|
||||
# The <tt>@target</tt> object is not \loaded until needed. For example,
|
||||
#
|
||||
# blog.posts.count
|
||||
#
|
||||
|
@ -57,76 +57,109 @@ module ActiveRecord
|
|||
reset
|
||||
end
|
||||
|
||||
# Returns the owner of the proxy.
|
||||
def proxy_owner
|
||||
@owner
|
||||
end
|
||||
|
||||
# Returns the reflection object that represents the association handled
|
||||
# by the proxy.
|
||||
def proxy_reflection
|
||||
@reflection
|
||||
end
|
||||
|
||||
# Returns the \target of the proxy, same as +target+.
|
||||
def proxy_target
|
||||
@target
|
||||
end
|
||||
|
||||
# Does the proxy or its \target respond to +symbol+?
|
||||
def respond_to?(*args)
|
||||
proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
|
||||
end
|
||||
|
||||
# Explicitly proxy === because the instance method removal above
|
||||
# doesn't catch it.
|
||||
# Forwards <tt>===</tt> explicitly to the \target because the instance method
|
||||
# removal above doesn't catch it. Loads the \target if needed.
|
||||
def ===(other)
|
||||
load_target
|
||||
other === @target
|
||||
end
|
||||
|
||||
# Returns the name of the table of the related class:
|
||||
#
|
||||
# post.comments.aliased_table_name # => "comments"
|
||||
#
|
||||
def aliased_table_name
|
||||
@reflection.klass.table_name
|
||||
end
|
||||
|
||||
# Returns the SQL string that corresponds to the <tt>:conditions</tt>
|
||||
# option of the macro, if given, or +nil+ otherwise.
|
||||
def conditions
|
||||
@conditions ||= interpolate_sql(sanitize_sql(@reflection.options[:conditions])) if @reflection.options[:conditions]
|
||||
@conditions ||= interpolate_sql(@reflection.sanitized_conditions) if @reflection.sanitized_conditions
|
||||
end
|
||||
alias :sql_conditions :conditions
|
||||
|
||||
# Resets the \loaded flag to +false+ and sets the \target to +nil+.
|
||||
def reset
|
||||
@loaded = false
|
||||
@target = nil
|
||||
end
|
||||
|
||||
# Reloads the \target and returns +self+ on success.
|
||||
def reload
|
||||
reset
|
||||
load_target
|
||||
self unless @target.nil?
|
||||
end
|
||||
|
||||
# Has the \target been already \loaded?
|
||||
def loaded?
|
||||
@loaded
|
||||
end
|
||||
|
||||
# Asserts the \target has been loaded setting the \loaded flag to +true+.
|
||||
def loaded
|
||||
@loaded = true
|
||||
end
|
||||
|
||||
# Returns the target of this proxy, same as +proxy_target+.
|
||||
def target
|
||||
@target
|
||||
end
|
||||
|
||||
# Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+.
|
||||
def target=(target)
|
||||
@target = target
|
||||
loaded
|
||||
end
|
||||
|
||||
# Forwards the call to the target. Loads the \target if needed.
|
||||
def inspect
|
||||
load_target
|
||||
@target.inspect
|
||||
end
|
||||
|
||||
def send(method, *args)
|
||||
if proxy_respond_to?(method)
|
||||
super
|
||||
else
|
||||
load_target
|
||||
@target.send(method, *args)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
# Does the association have a <tt>:dependent</tt> option?
|
||||
def dependent?
|
||||
@reflection.options[:dependent]
|
||||
end
|
||||
|
||||
# Returns a string with the IDs of +records+ joined with a comma, quoted
|
||||
# if needed. The result is ready to be inserted into a SQL IN clause.
|
||||
#
|
||||
# quoted_record_ids(records) # => "23,56,58,67"
|
||||
#
|
||||
def quoted_record_ids(records)
|
||||
records.map { |record| record.quoted_id }.join(',')
|
||||
end
|
||||
|
@ -135,10 +168,13 @@ module ActiveRecord
|
|||
@owner.send(:interpolate_sql, sql, record)
|
||||
end
|
||||
|
||||
# Forwards the call to the reflection class.
|
||||
def sanitize_sql(sql)
|
||||
@reflection.klass.send(:sanitize_sql, sql)
|
||||
end
|
||||
|
||||
# Assigns the ID of the owner to the corresponding foreign key in +record+.
|
||||
# If the association is polymorphic the type of the owner is also set.
|
||||
def set_belongs_to_association_for(record)
|
||||
if @reflection.options[:as]
|
||||
record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
|
||||
|
@ -148,6 +184,7 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Merges into +options+ the ones coming from the reflection.
|
||||
def merge_options_from_reflection!(options)
|
||||
options.reverse_merge!(
|
||||
:group => @reflection.options[:group],
|
||||
|
@ -160,13 +197,17 @@ module ActiveRecord
|
|||
)
|
||||
end
|
||||
|
||||
# Forwards +with_scope+ to the reflection.
|
||||
def with_scope(*args, &block)
|
||||
@reflection.klass.send :with_scope, *args, &block
|
||||
end
|
||||
|
||||
private
|
||||
# Forwards any missing method call to the \target.
|
||||
def method_missing(method, *args)
|
||||
if load_target
|
||||
raise NoMethodError unless @target.respond_to?(method)
|
||||
|
||||
if block_given?
|
||||
@target.send(method, *args) { |*block_args| yield(*block_args) }
|
||||
else
|
||||
|
@ -175,16 +216,16 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Loads the target if needed and returns it.
|
||||
# Loads the \target if needed and returns it.
|
||||
#
|
||||
# This method is abstract in the sense that it relies on +find_target+,
|
||||
# which is expected to be provided by descendants.
|
||||
#
|
||||
# If the target is already loaded it is just returned. Thus, you can call
|
||||
# +load_target+ unconditionally to get the target.
|
||||
# If the \target is already \loaded it is just returned. Thus, you can call
|
||||
# +load_target+ unconditionally to get the \target.
|
||||
#
|
||||
# ActiveRecord::RecordNotFound is rescued within the method, and it is
|
||||
# not reraised. The proxy is reset and +nil+ is the return value.
|
||||
# not reraised. The proxy is \reset and +nil+ is the return value.
|
||||
def load_target
|
||||
return nil unless defined?(@loaded)
|
||||
|
||||
|
@ -198,22 +239,33 @@ module ActiveRecord
|
|||
reset
|
||||
end
|
||||
|
||||
# Can be overwritten by associations that might have the foreign key available for an association without
|
||||
# having the object itself (and still being a new record). Currently, only belongs_to presents this scenario.
|
||||
# Can be overwritten by associations that might have the foreign key
|
||||
# available for an association without having the object itself (and
|
||||
# still being a new record). Currently, only +belongs_to+ presents
|
||||
# this scenario (both vanilla and polymorphic).
|
||||
def foreign_key_present
|
||||
false
|
||||
end
|
||||
|
||||
# Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
|
||||
# the kind of the class of the associated objects. Meant to be used as
|
||||
# a sanity check when you are about to assign an associated record.
|
||||
def raise_on_type_mismatch(record)
|
||||
unless record.is_a?(@reflection.klass)
|
||||
unless record.is_a?(@reflection.klass) || record.is_a?(@reflection.class_name.constantize)
|
||||
message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
|
||||
raise ActiveRecord::AssociationTypeMismatch, message
|
||||
end
|
||||
end
|
||||
|
||||
# Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
|
||||
# Array#flatten has problems with recursive arrays. Going one level
|
||||
# deeper solves the majority of the problems.
|
||||
def flatten_deeper(array)
|
||||
array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
|
||||
array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten
|
||||
end
|
||||
|
||||
# Returns the ID of the owner, quoted if needed.
|
||||
def owner_quoted_id
|
||||
@owner.quoted_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,11 +2,11 @@ module ActiveRecord
|
|||
module Associations
|
||||
class BelongsToAssociation < AssociationProxy #:nodoc:
|
||||
def create(attributes = {})
|
||||
replace(@reflection.klass.create(attributes))
|
||||
replace(@reflection.create_association(attributes))
|
||||
end
|
||||
|
||||
def build(attributes = {})
|
||||
replace(@reflection.klass.new(attributes))
|
||||
replace(@reflection.build_association(attributes))
|
||||
end
|
||||
|
||||
def replace(record)
|
||||
|
|
|
@ -37,7 +37,7 @@ module ActiveRecord
|
|||
attributes = columns.inject({}) do |attrs, column|
|
||||
case column.name.to_s
|
||||
when @reflection.primary_key_name.to_s
|
||||
attrs[column.name] = @owner.quoted_id
|
||||
attrs[column.name] = owner_quoted_id
|
||||
when @reflection.association_foreign_key.to_s
|
||||
attrs[column.name] = record.quoted_id
|
||||
else
|
||||
|
@ -64,7 +64,7 @@ module ActiveRecord
|
|||
records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) }
|
||||
else
|
||||
ids = quoted_record_ids(records)
|
||||
sql = "DELETE FROM #{@owner.connection.quote_table_name @reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})"
|
||||
sql = "DELETE FROM #{@owner.connection.quote_table_name @reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{owner_quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})"
|
||||
@owner.connection.delete(sql)
|
||||
end
|
||||
end
|
||||
|
@ -73,11 +73,21 @@ module ActiveRecord
|
|||
if @reflection.options[:finder_sql]
|
||||
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
|
||||
else
|
||||
@finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} "
|
||||
@finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
|
||||
@finder_sql << " AND (#{conditions})" if conditions
|
||||
end
|
||||
|
||||
@join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
|
||||
|
||||
if @reflection.options[:counter_sql]
|
||||
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
||||
elsif @reflection.options[:finder_sql]
|
||||
# replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
|
||||
@reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
|
||||
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
||||
else
|
||||
@counter_sql = @finder_sql
|
||||
end
|
||||
end
|
||||
|
||||
def construct_scope
|
||||
|
|
|
@ -1,33 +1,32 @@
|
|||
module ActiveRecord
|
||||
module Associations
|
||||
# This is the proxy that handles a has many association.
|
||||
#
|
||||
# If the association has a <tt>:through</tt> option further specialization
|
||||
# is provided by its child HasManyThroughAssociation.
|
||||
class HasManyAssociation < AssociationCollection #:nodoc:
|
||||
# Count the number of associated records. All arguments are optional.
|
||||
def count(*args)
|
||||
if @reflection.options[:counter_sql]
|
||||
@reflection.klass.count_by_sql(@counter_sql)
|
||||
elsif @reflection.options[:finder_sql]
|
||||
@reflection.klass.count_by_sql(@finder_sql)
|
||||
else
|
||||
column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
|
||||
options[:conditions] = options[:conditions].blank? ?
|
||||
@finder_sql :
|
||||
@finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
|
||||
options[:include] ||= @reflection.options[:include]
|
||||
|
||||
value = @reflection.klass.count(column_name, options)
|
||||
|
||||
limit = @reflection.options[:limit]
|
||||
offset = @reflection.options[:offset]
|
||||
|
||||
if limit || offset
|
||||
[ [value - offset.to_i, 0].max, limit.to_i ].min
|
||||
protected
|
||||
def owner_quoted_id
|
||||
if @reflection.options[:primary_key]
|
||||
quote_value(@owner.send(@reflection.options[:primary_key]))
|
||||
else
|
||||
value
|
||||
@owner.quoted_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
# Returns the number of records in this collection.
|
||||
#
|
||||
# If the association has a counter cache it gets that value. Otherwise
|
||||
# it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
|
||||
# there's one. Some configuration options like :group make it impossible
|
||||
# to do a SQL count, in those cases the array count will be used.
|
||||
#
|
||||
# That does not depend on whether the collection has already been loaded
|
||||
# or not. The +size+ method is the one that takes the loaded flag into
|
||||
# account and delegates to +count_records+ if needed.
|
||||
#
|
||||
# If the collection is empty the target is set to an empty array and
|
||||
# the loaded flag is set to true as well.
|
||||
def count_records
|
||||
count = if has_cached_counter?
|
||||
@owner.send(:read_attribute, cached_counter_attribute_name)
|
||||
|
@ -62,17 +61,18 @@ module ActiveRecord
|
|||
record.save
|
||||
end
|
||||
|
||||
# Deletes the records according to the <tt>:dependent</tt> option.
|
||||
def delete_records(records)
|
||||
case @reflection.options[:dependent]
|
||||
when :destroy
|
||||
records.each(&:destroy)
|
||||
records.each { |r| r.destroy }
|
||||
when :delete_all
|
||||
@reflection.klass.delete(records.map(&:id))
|
||||
@reflection.klass.delete(records.map { |record| record.id })
|
||||
else
|
||||
ids = quoted_record_ids(records)
|
||||
@reflection.klass.update_all(
|
||||
"#{@reflection.primary_key_name} = NULL",
|
||||
"#{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})"
|
||||
"#{@reflection.primary_key_name} = #{owner_quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -88,12 +88,12 @@ module ActiveRecord
|
|||
|
||||
when @reflection.options[:as]
|
||||
@finder_sql =
|
||||
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
|
||||
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
|
||||
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
|
||||
@finder_sql << " AND (#{conditions})" if conditions
|
||||
|
||||
else
|
||||
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
|
||||
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
|
||||
@finder_sql << " AND (#{conditions})" if conditions
|
||||
end
|
||||
|
||||
|
|
|
@ -9,15 +9,15 @@ module ActiveRecord
|
|||
alias_method :new, :build
|
||||
|
||||
def create!(attrs = nil)
|
||||
@reflection.klass.transaction do
|
||||
self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create! } : @reflection.klass.create!)
|
||||
transaction do
|
||||
self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association! } : @reflection.create_association!)
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
def create(attrs = nil)
|
||||
@reflection.klass.transaction do
|
||||
self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create } : @reflection.klass.create)
|
||||
transaction do
|
||||
self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association } : @reflection.create_association)
|
||||
object
|
||||
end
|
||||
end
|
||||
|
@ -31,17 +31,15 @@ module ActiveRecord
|
|||
return count
|
||||
end
|
||||
|
||||
def count(*args)
|
||||
column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
|
||||
if @reflection.options[:uniq]
|
||||
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL statement.
|
||||
column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all
|
||||
options.merge!(:distinct => true)
|
||||
end
|
||||
@reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
|
||||
end
|
||||
|
||||
protected
|
||||
def target_reflection_has_associated_record?
|
||||
if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank?
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def construct_find_options!(options)
|
||||
options[:select] = construct_select(options[:select])
|
||||
options[:from] ||= construct_from
|
||||
|
@ -57,8 +55,9 @@ module ActiveRecord
|
|||
return false unless record.save
|
||||
end
|
||||
end
|
||||
klass = @reflection.through_reflection.klass
|
||||
@owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { klass.create! }
|
||||
through_reflection = @reflection.through_reflection
|
||||
klass = through_reflection.klass
|
||||
@owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { through_reflection.create_association! }
|
||||
end
|
||||
|
||||
# TODO - add dependent option support
|
||||
|
@ -70,6 +69,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def find_target
|
||||
return [] unless target_reflection_has_associated_record?
|
||||
@reflection.klass.find(:all,
|
||||
:select => construct_select,
|
||||
:conditions => construct_conditions,
|
||||
|
@ -107,12 +107,14 @@ module ActiveRecord
|
|||
# Associate attributes pointing to owner, quoted.
|
||||
def construct_quoted_owner_attributes(reflection)
|
||||
if as = reflection.options[:as]
|
||||
{ "#{as}_id" => @owner.quoted_id,
|
||||
{ "#{as}_id" => owner_quoted_id,
|
||||
"#{as}_type" => reflection.klass.quote_value(
|
||||
@owner.class.base_class.name.to_s,
|
||||
reflection.klass.columns_hash["#{as}_type"]) }
|
||||
elsif reflection.macro == :belongs_to
|
||||
{ reflection.klass.primary_key => @owner[reflection.primary_key_name] }
|
||||
else
|
||||
{ reflection.primary_key_name => @owner.quoted_id }
|
||||
{ reflection.primary_key_name => owner_quoted_id }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -183,7 +185,7 @@ module ActiveRecord
|
|||
when @reflection.options[:finder_sql]
|
||||
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
|
||||
|
||||
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
|
||||
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
|
||||
@finder_sql << " AND (#{conditions})" if conditions
|
||||
else
|
||||
@finder_sql = construct_conditions
|
||||
|
|
|
@ -7,15 +7,21 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def create(attrs = {}, replace_existing = true)
|
||||
new_record(replace_existing) { |klass| klass.create(attrs) }
|
||||
new_record(replace_existing) do |reflection|
|
||||
reflection.create_association(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
def create!(attrs = {}, replace_existing = true)
|
||||
new_record(replace_existing) { |klass| klass.create!(attrs) }
|
||||
new_record(replace_existing) do |reflection|
|
||||
reflection.create_association!(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
def build(attrs = {}, replace_existing = true)
|
||||
new_record(replace_existing) { |klass| klass.new(attrs) }
|
||||
new_record(replace_existing) do |reflection|
|
||||
reflection.build_association(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
def replace(obj, dont_save = false)
|
||||
|
@ -47,7 +53,16 @@ module ActiveRecord
|
|||
return (obj.nil? ? nil : self)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
protected
|
||||
def owner_quoted_id
|
||||
if @reflection.options[:primary_key]
|
||||
@owner.class.quote_value(@owner.send(@reflection.options[:primary_key]))
|
||||
else
|
||||
@owner.quoted_id
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def find_target
|
||||
@reflection.klass.find(:first,
|
||||
|
@ -63,10 +78,10 @@ module ActiveRecord
|
|||
case
|
||||
when @reflection.options[:as]
|
||||
@finder_sql =
|
||||
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
|
||||
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
|
||||
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
|
||||
else
|
||||
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
|
||||
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
|
||||
end
|
||||
@finder_sql << " AND (#{conditions})" if conditions
|
||||
end
|
||||
|
@ -82,7 +97,9 @@ module ActiveRecord
|
|||
# instance. Otherwise, if the target has not previously been loaded
|
||||
# elsewhere, the instance we create will get orphaned.
|
||||
load_target if replace_existing
|
||||
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { yield @reflection.klass }
|
||||
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
|
||||
yield @reflection
|
||||
end
|
||||
|
||||
if replace_existing
|
||||
replace(record, true)
|
||||
|
|
|
@ -10,7 +10,7 @@ module ActiveRecord
|
|||
base.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
|
||||
base.cattr_accessor :time_zone_aware_attributes, :instance_writer => false
|
||||
base.time_zone_aware_attributes = false
|
||||
base.cattr_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
|
||||
base.class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
|
||||
base.skip_time_zone_conversion_for_attributes = []
|
||||
end
|
||||
|
||||
|
@ -214,7 +214,7 @@ module ActiveRecord
|
|||
if logger
|
||||
logger.warn "Exception occurred during reader method compilation."
|
||||
logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
|
||||
logger.warn "#{err.message}"
|
||||
logger.warn err.message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -232,6 +232,10 @@ module ActiveRecord
|
|||
def method_missing(method_id, *args, &block)
|
||||
method_name = method_id.to_s
|
||||
|
||||
if self.class.private_method_defined?(method_name)
|
||||
raise NoMethodError("Attempt to call private method", method_name, args)
|
||||
end
|
||||
|
||||
# If we haven't generated any methods yet, generate them, then
|
||||
# see if we've created the method we're looking for.
|
||||
if !self.class.generated_methods?
|
||||
|
@ -330,14 +334,18 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# A Person object with a name attribute can ask <tt>person.respond_to?("name")</tt>,
|
||||
# <tt>person.respond_to?("name=")</tt>, and <tt>person.respond_to?("name?")</tt>
|
||||
# A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
|
||||
# <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
|
||||
# which will all return +true+.
|
||||
alias :respond_to_without_attributes? :respond_to?
|
||||
def respond_to?(method, include_priv = false)
|
||||
def respond_to?(method, include_private_methods = false)
|
||||
method_name = method.to_s
|
||||
if super
|
||||
return true
|
||||
elsif !include_private_methods && super(method, true)
|
||||
# If we're here than we haven't found among non-private methods
|
||||
# but found among all methods. Which means that given method is private.
|
||||
return false
|
||||
elsif !self.class.generated_methods?
|
||||
self.class.define_attribute_methods
|
||||
if self.class.generated_methods.include?(method_name)
|
||||
|
|
623
vendor/rails/activerecord/lib/active_record/base.rb
vendored
623
vendor/rails/activerecord/lib/active_record/base.rb
vendored
File diff suppressed because it is too large
Load diff
|
@ -14,7 +14,7 @@ module ActiveRecord
|
|||
#
|
||||
# The third approach, count using options, accepts an option hash as the only parameter. The options are:
|
||||
#
|
||||
# * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro.
|
||||
# * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base.
|
||||
# * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed)
|
||||
# or named associations in the same form used for the <tt>:include</tt> option, which will perform an INNER JOIN on the associated table(s).
|
||||
# If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns.
|
||||
|
@ -98,7 +98,7 @@ module ActiveRecord
|
|||
# end
|
||||
#
|
||||
# Options:
|
||||
# * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro.
|
||||
# * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base.
|
||||
# * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, the purpose of this is to access fields on joined tables in your conditions, order, or group clauses.
|
||||
# * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
|
||||
# The records will be returned read-only since they will have attributes that do not correspond to the table's columns.
|
||||
|
@ -188,7 +188,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
joins = ""
|
||||
add_joins!(joins, options, scope)
|
||||
add_joins!(joins, options[:joins], scope)
|
||||
|
||||
if merged_includes.any?
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, joins)
|
||||
|
@ -217,7 +217,7 @@ module ActiveRecord
|
|||
|
||||
sql << " ORDER BY #{options[:order]} " if options[:order]
|
||||
add_limit!(sql, options, scope)
|
||||
sql << ') AS #{aggregate_alias}_subquery' if use_workaround
|
||||
sql << ") #{aggregate_alias}_subquery" if use_workaround
|
||||
sql
|
||||
end
|
||||
|
||||
|
@ -266,7 +266,14 @@ module ActiveRecord
|
|||
# column_alias_for("count(*)") # => "count_all"
|
||||
# column_alias_for("count", "id") # => "count_id"
|
||||
def column_alias_for(*keys)
|
||||
connection.table_alias_for(keys.join(' ').downcase.gsub(/\*/, 'all').gsub(/\W+/, ' ').strip.gsub(/ +/, '_'))
|
||||
table_name = keys.join(' ')
|
||||
table_name.downcase!
|
||||
table_name.gsub!(/\*/, 'all')
|
||||
table_name.gsub!(/\W+/, ' ')
|
||||
table_name.strip!
|
||||
table_name.gsub!(/ +/, '_')
|
||||
|
||||
connection.table_alias_for(table_name)
|
||||
end
|
||||
|
||||
def column_for(field)
|
||||
|
@ -278,11 +285,15 @@ module ActiveRecord
|
|||
operation = operation.to_s.downcase
|
||||
case operation
|
||||
when 'count' then value.to_i
|
||||
when 'sum' then value =~ /\./ ? value.to_f : value.to_i
|
||||
when 'avg' then value && value.to_f
|
||||
else column ? column.type_cast(value) : value
|
||||
when 'sum' then type_cast_using_column(value || '0', column)
|
||||
when 'avg' then value && value.to_d
|
||||
else type_cast_using_column(value, column)
|
||||
end
|
||||
end
|
||||
|
||||
def type_cast_using_column(value, column)
|
||||
column ? column.type_cast(value) : value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,9 +3,9 @@ require 'observer'
|
|||
module ActiveRecord
|
||||
# Callbacks are hooks into the lifecycle of an Active Record object that allow you to trigger logic
|
||||
# before or after an alteration of the object state. This can be used to make sure that associated and
|
||||
# dependent objects are deleted when destroy is called (by overwriting +before_destroy+) or to massage attributes
|
||||
# dependent objects are deleted when +destroy+ is called (by overwriting +before_destroy+) or to massage attributes
|
||||
# before they're validated (by overwriting +before_validation+). As an example of the callbacks initiated, consider
|
||||
# the <tt>Base#save</tt> call:
|
||||
# the <tt>Base#save</tt> call for a new record:
|
||||
#
|
||||
# * (-) <tt>save</tt>
|
||||
# * (-) <tt>valid</tt>
|
||||
|
@ -22,7 +22,8 @@ module ActiveRecord
|
|||
# * (8) <tt>after_save</tt>
|
||||
#
|
||||
# That's a total of eight callbacks, which gives you immense power to react and prepare for each state in the
|
||||
# Active Record lifecycle.
|
||||
# Active Record lifecycle. The sequence for calling <tt>Base#save</tt> an existing record is similar, except that each
|
||||
# <tt>_on_create</tt> callback is replaced by the corresponding <tt>_on_update</tt> callback.
|
||||
#
|
||||
# Examples:
|
||||
# class CreditCard < ActiveRecord::Base
|
||||
|
@ -50,7 +51,7 @@ module ActiveRecord
|
|||
#
|
||||
# == Inheritable callback queues
|
||||
#
|
||||
# Besides the overwriteable callback methods, it's also possible to register callbacks through the use of the callback macros.
|
||||
# Besides the overwritable callback methods, it's also possible to register callbacks through the use of the callback macros.
|
||||
# Their main advantage is that the macros add behavior into a callback queue that is kept intact down through an inheritance
|
||||
# hierarchy. Example:
|
||||
#
|
||||
|
@ -161,7 +162,7 @@ module ActiveRecord
|
|||
# == <tt>before_validation*</tt> returning statements
|
||||
#
|
||||
# If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be aborted and <tt>Base#save</tt> will return +false+.
|
||||
# If Base#save! is called it will raise a RecordNotSaved exception.
|
||||
# If Base#save! is called it will raise a ActiveRecord::RecordInvalid exception.
|
||||
# Nothing will be appended to the errors object.
|
||||
#
|
||||
# == Canceling callbacks
|
||||
|
@ -169,6 +170,18 @@ module ActiveRecord
|
|||
# If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are cancelled. If an <tt>after_*</tt> callback returns
|
||||
# +false+, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
|
||||
# defined as methods on the model, which are called last.
|
||||
#
|
||||
# == Transactions
|
||||
#
|
||||
# The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs
|
||||
# within a transaction. That includes <tt>after_*</tt> hooks. If everything
|
||||
# goes fine a COMMIT is executed once the chain has been completed.
|
||||
#
|
||||
# If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You
|
||||
# can also trigger a ROLLBACK raising an exception in any of the callbacks,
|
||||
# including <tt>after_*</tt> hooks. Note, however, that in that case the client
|
||||
# needs to be aware of it because an ordinary +save+ will raise such exception
|
||||
# instead of quietly returning +false+.
|
||||
module Callbacks
|
||||
CALLBACKS = %w(
|
||||
after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
|
||||
|
@ -197,6 +210,8 @@ module ActiveRecord
|
|||
def before_save() end
|
||||
|
||||
# Is called _after_ <tt>Base.save</tt> (regardless of whether it's a +create+ or +update+ save).
|
||||
# Note that this callback is still wrapped in the transaction around +save+. For example, if you
|
||||
# invoke an external indexer at this point it won't see the changes in the database.
|
||||
#
|
||||
# class Contact < ActiveRecord::Base
|
||||
# after_save { logger.info( 'New contact saved!' ) }
|
||||
|
@ -214,6 +229,8 @@ module ActiveRecord
|
|||
def before_create() end
|
||||
|
||||
# Is called _after_ <tt>Base.save</tt> on new objects that haven't been saved yet (no record exists).
|
||||
# Note that this callback is still wrapped in the transaction around +save+. For example, if you
|
||||
# invoke an external indexer at this point it won't see the changes in the database.
|
||||
def after_create() end
|
||||
def create_with_callbacks #:nodoc:
|
||||
return false if callback(:before_create) == false
|
||||
|
@ -227,6 +244,8 @@ module ActiveRecord
|
|||
def before_update() end
|
||||
|
||||
# Is called _after_ <tt>Base.save</tt> on existing objects that have a record.
|
||||
# Note that this callback is still wrapped in the transaction around +save+. For example, if you
|
||||
# invoke an external indexer at this point it won't see the changes in the database.
|
||||
def after_update() end
|
||||
|
||||
def update_with_callbacks(*args) #:nodoc:
|
||||
|
@ -262,7 +281,7 @@ module ActiveRecord
|
|||
def valid_with_callbacks? #:nodoc:
|
||||
return false if callback(:before_validation) == false
|
||||
if new_record? then result = callback(:before_validation_on_create) else result = callback(:before_validation_on_update) end
|
||||
return false if result == false
|
||||
return false if false == result
|
||||
|
||||
result = valid_without_callbacks?
|
||||
|
||||
|
@ -293,14 +312,14 @@ module ActiveRecord
|
|||
|
||||
private
|
||||
def callback(method)
|
||||
notify(method)
|
||||
|
||||
result = run_callbacks(method) { |result, object| result == false }
|
||||
result = run_callbacks(method) { |result, object| false == result }
|
||||
|
||||
if result != false && respond_to_without_attributes?(method)
|
||||
result = send(method)
|
||||
end
|
||||
|
||||
notify(method)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
|
|
349
vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
vendored
Normal file
349
vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
vendored
Normal file
|
@ -0,0 +1,349 @@
|
|||
require 'monitor'
|
||||
require 'set'
|
||||
|
||||
module ActiveRecord
|
||||
# Raised when a connection could not be obtained within the connection
|
||||
# acquisition timeout period.
|
||||
class ConnectionTimeoutError < ConnectionNotEstablished
|
||||
end
|
||||
|
||||
module ConnectionAdapters
|
||||
# Connection pool base class for managing ActiveRecord database
|
||||
# connections.
|
||||
#
|
||||
# == Introduction
|
||||
#
|
||||
# A connection pool synchronizes thread access to a limited number of
|
||||
# database connections. The basic idea is that each thread checks out a
|
||||
# database connection from the pool, uses that connection, and checks the
|
||||
# connection back in. ConnectionPool is completely thread-safe, and will
|
||||
# ensure that a connection cannot be used by two threads at the same time,
|
||||
# as long as ConnectionPool's contract is correctly followed. It will also
|
||||
# handle cases in which there are more threads than connections: if all
|
||||
# connections have been checked out, and a thread tries to checkout a
|
||||
# connection anyway, then ConnectionPool will wait until some other thread
|
||||
# has checked in a connection.
|
||||
#
|
||||
# == Obtaining (checking out) a connection
|
||||
#
|
||||
# Connections can be obtained and used from a connection pool in several
|
||||
# ways:
|
||||
#
|
||||
# 1. Simply use ActiveRecord::Base.connection as with ActiveRecord 2.1 and
|
||||
# earlier (pre-connection-pooling). Eventually, when you're done with
|
||||
# the connection(s) and wish it to be returned to the pool, you call
|
||||
# ActiveRecord::Base.clear_active_connections!. This will be the
|
||||
# default behavior for ActiveRecord when used in conjunction with
|
||||
# ActionPack's request handling cycle.
|
||||
# 2. Manually check out a connection from the pool with
|
||||
# ActiveRecord::Base.connection_pool.checkout. You are responsible for
|
||||
# returning this connection to the pool when finished by calling
|
||||
# ActiveRecord::Base.connection_pool.checkin(connection).
|
||||
# 3. Use ActiveRecord::Base.connection_pool.with_connection(&block), which
|
||||
# obtains a connection, yields it as the sole argument to the block,
|
||||
# and returns it to the pool after the block completes.
|
||||
#
|
||||
# Connections in the pool are actually AbstractAdapter objects (or objects
|
||||
# compatible with AbstractAdapter's interface).
|
||||
#
|
||||
# == Options
|
||||
#
|
||||
# There are two connection-pooling-related options that you can add to
|
||||
# your database connection configuration:
|
||||
#
|
||||
# * +pool+: number indicating size of connection pool (default 5)
|
||||
# * +wait_timeout+: number of seconds to block and wait for a connection
|
||||
# before giving up and raising a timeout error (default 5 seconds).
|
||||
class ConnectionPool
|
||||
attr_reader :spec
|
||||
|
||||
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
|
||||
# object which describes database connection information (e.g. adapter,
|
||||
# host name, username, password, etc), as well as the maximum size for
|
||||
# this ConnectionPool.
|
||||
#
|
||||
# The default ConnectionPool maximum size is 5.
|
||||
def initialize(spec)
|
||||
@spec = spec
|
||||
# The cache of reserved connections mapped to threads
|
||||
@reserved_connections = {}
|
||||
# The mutex used to synchronize pool access
|
||||
@connection_mutex = Monitor.new
|
||||
@queue = @connection_mutex.new_cond
|
||||
# default 5 second timeout
|
||||
@timeout = spec.config[:wait_timeout] || 5
|
||||
# default max pool size to 5
|
||||
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
|
||||
@connections = []
|
||||
@checked_out = []
|
||||
end
|
||||
|
||||
# Retrieve the connection associated with the current thread, or call
|
||||
# #checkout to obtain one if necessary.
|
||||
#
|
||||
# #connection can be called any number of times; the connection is
|
||||
# held in a hash keyed by the thread id.
|
||||
def connection
|
||||
if conn = @reserved_connections[current_connection_id]
|
||||
conn
|
||||
else
|
||||
@reserved_connections[current_connection_id] = checkout
|
||||
end
|
||||
end
|
||||
|
||||
# Signal that the thread is finished with the current connection.
|
||||
# #release_connection releases the connection-thread association
|
||||
# and returns the connection to the pool.
|
||||
def release_connection
|
||||
conn = @reserved_connections.delete(current_connection_id)
|
||||
checkin conn if conn
|
||||
end
|
||||
|
||||
# Reserve a connection, and yield it to a block. Ensure the connection is
|
||||
# checked back in when finished.
|
||||
def with_connection
|
||||
conn = checkout
|
||||
yield conn
|
||||
ensure
|
||||
checkin conn
|
||||
end
|
||||
|
||||
# Returns true if a connection has already been opened.
|
||||
def connected?
|
||||
!@connections.empty?
|
||||
end
|
||||
|
||||
# Disconnects all connections in the pool, and clears the pool.
|
||||
def disconnect!
|
||||
@reserved_connections.each do |name,conn|
|
||||
checkin conn
|
||||
end
|
||||
@reserved_connections = {}
|
||||
@connections.each do |conn|
|
||||
conn.disconnect!
|
||||
end
|
||||
@connections = []
|
||||
end
|
||||
|
||||
# Clears the cache which maps classes
|
||||
def clear_reloadable_connections!
|
||||
@reserved_connections.each do |name, conn|
|
||||
checkin conn
|
||||
end
|
||||
@reserved_connections = {}
|
||||
@connections.each do |conn|
|
||||
conn.disconnect! if conn.requires_reloading?
|
||||
end
|
||||
@connections = []
|
||||
end
|
||||
|
||||
# Verify active connections and remove and disconnect connections
|
||||
# associated with stale threads.
|
||||
def verify_active_connections! #:nodoc:
|
||||
clear_stale_cached_connections!
|
||||
@connections.each do |connection|
|
||||
connection.verify!
|
||||
end
|
||||
end
|
||||
|
||||
# Return any checked-out connections back to the pool by threads that
|
||||
# are no longer alive.
|
||||
def clear_stale_cached_connections!
|
||||
remove_stale_cached_threads!(@reserved_connections) do |name, conn|
|
||||
checkin conn
|
||||
end
|
||||
end
|
||||
|
||||
# Check-out a database connection from the pool, indicating that you want
|
||||
# to use it. You should call #checkin when you no longer need this.
|
||||
#
|
||||
# This is done by either returning an existing connection, or by creating
|
||||
# a new connection. If the maximum number of connections for this pool has
|
||||
# already been reached, but the pool is empty (i.e. they're all being used),
|
||||
# then this method will wait until a thread has checked in a connection.
|
||||
# The wait time is bounded however: if no connection can be checked out
|
||||
# within the timeout specified for this pool, then a ConnectionTimeoutError
|
||||
# exception will be raised.
|
||||
#
|
||||
# Returns: an AbstractAdapter object.
|
||||
#
|
||||
# Raises:
|
||||
# - ConnectionTimeoutError: no connection can be obtained from the pool
|
||||
# within the timeout period.
|
||||
def checkout
|
||||
# Checkout an available connection
|
||||
@connection_mutex.synchronize do
|
||||
loop do
|
||||
conn = if @checked_out.size < @connections.size
|
||||
checkout_existing_connection
|
||||
elsif @connections.size < @size
|
||||
checkout_new_connection
|
||||
end
|
||||
return conn if conn
|
||||
# No connections available; wait for one
|
||||
if @queue.wait(@timeout)
|
||||
next
|
||||
else
|
||||
# try looting dead threads
|
||||
clear_stale_cached_connections!
|
||||
if @size == @checked_out.size
|
||||
raise ConnectionTimeoutError, "could not obtain a database connection within #{@timeout} seconds. The pool size is currently #{@size}, perhaps you need to increase it?"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Check-in a database connection back into the pool, indicating that you
|
||||
# no longer need this connection.
|
||||
#
|
||||
# +conn+: an AbstractAdapter object, which was obtained by earlier by
|
||||
# calling +checkout+ on this pool.
|
||||
def checkin(conn)
|
||||
@connection_mutex.synchronize do
|
||||
conn.run_callbacks :checkin
|
||||
@checked_out.delete conn
|
||||
@queue.signal
|
||||
end
|
||||
end
|
||||
|
||||
synchronize :clear_reloadable_connections!, :verify_active_connections!,
|
||||
:connected?, :disconnect!, :with => :@connection_mutex
|
||||
|
||||
private
|
||||
def new_connection
|
||||
ActiveRecord::Base.send(spec.adapter_method, spec.config)
|
||||
end
|
||||
|
||||
def current_connection_id #:nodoc:
|
||||
Thread.current.object_id
|
||||
end
|
||||
|
||||
# Remove stale threads from the cache.
|
||||
def remove_stale_cached_threads!(cache, &block)
|
||||
keys = Set.new(cache.keys)
|
||||
|
||||
Thread.list.each do |thread|
|
||||
keys.delete(thread.object_id) if thread.alive?
|
||||
end
|
||||
keys.each do |key|
|
||||
next unless cache.has_key?(key)
|
||||
block.call(key, cache[key])
|
||||
cache.delete(key)
|
||||
end
|
||||
end
|
||||
|
||||
def checkout_new_connection
|
||||
c = new_connection
|
||||
@connections << c
|
||||
checkout_and_verify(c)
|
||||
end
|
||||
|
||||
def checkout_existing_connection
|
||||
c = (@connections - @checked_out).first
|
||||
checkout_and_verify(c)
|
||||
end
|
||||
|
||||
def checkout_and_verify(c)
|
||||
c.verify!
|
||||
c.run_callbacks :checkout
|
||||
@checked_out << c
|
||||
c
|
||||
end
|
||||
end
|
||||
|
||||
# ConnectionHandler is a collection of ConnectionPool objects. It is used
|
||||
# for keeping separate connection pools for ActiveRecord models that connect
|
||||
# to different databases.
|
||||
#
|
||||
# For example, suppose that you have 5 models, with the following hierarchy:
|
||||
#
|
||||
# |
|
||||
# +-- Book
|
||||
# | |
|
||||
# | +-- ScaryBook
|
||||
# | +-- GoodBook
|
||||
# +-- Author
|
||||
# +-- BankAccount
|
||||
#
|
||||
# Suppose that Book is to connect to a separate database (i.e. one other
|
||||
# than the default database). Then Book, ScaryBook and GoodBook will all use
|
||||
# the same connection pool. Likewise, Author and BankAccount will use the
|
||||
# same connection pool. However, the connection pool used by Author/BankAccount
|
||||
# is not the same as the one used by Book/ScaryBook/GoodBook.
|
||||
#
|
||||
# Normally there is only a single ConnectionHandler instance, accessible via
|
||||
# ActiveRecord::Base.connection_handler. ActiveRecord models use this to
|
||||
# determine that connection pool that they should use.
|
||||
class ConnectionHandler
|
||||
def initialize(pools = {})
|
||||
@connection_pools = pools
|
||||
end
|
||||
|
||||
def connection_pools
|
||||
@connection_pools ||= {}
|
||||
end
|
||||
|
||||
def establish_connection(name, spec)
|
||||
@connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec)
|
||||
end
|
||||
|
||||
# Returns any connections in use by the current thread back to the pool,
|
||||
# and also returns connections to the pool cached by threads that are no
|
||||
# longer alive.
|
||||
def clear_active_connections!
|
||||
@connection_pools.each_value do |pool|
|
||||
pool.release_connection
|
||||
pool.clear_stale_cached_connections!
|
||||
end
|
||||
end
|
||||
|
||||
# Clears the cache which maps classes
|
||||
def clear_reloadable_connections!
|
||||
@connection_pools.each_value {|pool| pool.clear_reloadable_connections! }
|
||||
end
|
||||
|
||||
def clear_all_connections!
|
||||
@connection_pools.each_value {|pool| pool.disconnect! }
|
||||
end
|
||||
|
||||
# Verify active connections.
|
||||
def verify_active_connections! #:nodoc:
|
||||
@connection_pools.each_value {|pool| pool.verify_active_connections! }
|
||||
end
|
||||
|
||||
# Locate the connection of the nearest super class. This can be an
|
||||
# active or defined connection: if it is the latter, it will be
|
||||
# opened and set as the active connection for the class it was defined
|
||||
# for (not necessarily the current class).
|
||||
def retrieve_connection(klass) #:nodoc:
|
||||
pool = retrieve_connection_pool(klass)
|
||||
(pool && pool.connection) or raise ConnectionNotEstablished
|
||||
end
|
||||
|
||||
# Returns true if a connection that's accessible to this class has
|
||||
# already been opened.
|
||||
def connected?(klass)
|
||||
retrieve_connection_pool(klass).connected?
|
||||
end
|
||||
|
||||
# Remove the connection for this class. This will close the active
|
||||
# connection and the defined connection (if they exist). The result
|
||||
# can be used as an argument for establish_connection, for easily
|
||||
# re-establishing the connection.
|
||||
def remove_connection(klass)
|
||||
pool = @connection_pools[klass.name]
|
||||
@connection_pools.delete_if { |key, value| value == pool }
|
||||
pool.disconnect! if pool
|
||||
pool.spec.config if pool
|
||||
end
|
||||
|
||||
def retrieve_connection_pool(klass)
|
||||
pool = @connection_pools[klass.name]
|
||||
return pool if pool
|
||||
return nil if ActiveRecord::Base == klass
|
||||
retrieve_connection_pool klass.superclass
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,3 @@
|
|||
require 'set'
|
||||
|
||||
module ActiveRecord
|
||||
class Base
|
||||
class ConnectionSpecification #:nodoc:
|
||||
|
@ -9,163 +7,9 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Check for activity after at least +verification_timeout+ seconds.
|
||||
# Defaults to 0 (always check.)
|
||||
cattr_accessor :verification_timeout, :instance_writer => false
|
||||
@@verification_timeout = 0
|
||||
|
||||
# The class -> [adapter_method, config] map
|
||||
@@defined_connections = {}
|
||||
|
||||
# The class -> thread id -> adapter cache. (class -> adapter if not allow_concurrency)
|
||||
@@active_connections = {}
|
||||
|
||||
class << self
|
||||
# Retrieve the connection cache.
|
||||
def thread_safe_active_connections #:nodoc:
|
||||
@@active_connections[Thread.current.object_id] ||= {}
|
||||
end
|
||||
|
||||
def single_threaded_active_connections #:nodoc:
|
||||
@@active_connections
|
||||
end
|
||||
|
||||
# pick up the right active_connection method from @@allow_concurrency
|
||||
if @@allow_concurrency
|
||||
alias_method :active_connections, :thread_safe_active_connections
|
||||
else
|
||||
alias_method :active_connections, :single_threaded_active_connections
|
||||
end
|
||||
|
||||
# set concurrency support flag (not thread safe, like most of the methods in this file)
|
||||
def allow_concurrency=(threaded) #:nodoc:
|
||||
logger.debug "allow_concurrency=#{threaded}" if logger
|
||||
return if @@allow_concurrency == threaded
|
||||
clear_all_cached_connections!
|
||||
@@allow_concurrency = threaded
|
||||
method_prefix = threaded ? "thread_safe" : "single_threaded"
|
||||
sing = (class << self; self; end)
|
||||
[:active_connections, :scoped_methods].each do |method|
|
||||
sing.send(:alias_method, method, "#{method_prefix}_#{method}")
|
||||
end
|
||||
log_connections if logger
|
||||
end
|
||||
|
||||
def active_connection_name #:nodoc:
|
||||
@active_connection_name ||=
|
||||
if active_connections[name] || @@defined_connections[name]
|
||||
name
|
||||
elsif self == ActiveRecord::Base
|
||||
nil
|
||||
else
|
||||
superclass.active_connection_name
|
||||
end
|
||||
end
|
||||
|
||||
def clear_active_connection_name #:nodoc:
|
||||
@active_connection_name = nil
|
||||
subclasses.each { |klass| klass.clear_active_connection_name }
|
||||
end
|
||||
|
||||
# Returns the connection currently associated with the class. This can
|
||||
# also be used to "borrow" the connection to do database work unrelated
|
||||
# to any of the specific Active Records.
|
||||
def connection
|
||||
if defined?(@active_connection_name) && (conn = active_connections[@active_connection_name])
|
||||
conn
|
||||
else
|
||||
# retrieve_connection sets the cache key.
|
||||
conn = retrieve_connection
|
||||
active_connections[@active_connection_name] = conn
|
||||
end
|
||||
end
|
||||
|
||||
# Clears the cache which maps classes to connections.
|
||||
def clear_active_connections!
|
||||
clear_cache!(@@active_connections) do |name, conn|
|
||||
conn.disconnect!
|
||||
end
|
||||
end
|
||||
|
||||
# Clears the cache which maps classes
|
||||
def clear_reloadable_connections!
|
||||
if @@allow_concurrency
|
||||
# With concurrent connections @@active_connections is
|
||||
# a hash keyed by thread id.
|
||||
@@active_connections.each do |thread_id, conns|
|
||||
conns.each do |name, conn|
|
||||
if conn.requires_reloading?
|
||||
conn.disconnect!
|
||||
@@active_connections[thread_id].delete(name)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
@@active_connections.each do |name, conn|
|
||||
if conn.requires_reloading?
|
||||
conn.disconnect!
|
||||
@@active_connections.delete(name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Verify active connections.
|
||||
def verify_active_connections! #:nodoc:
|
||||
if @@allow_concurrency
|
||||
remove_stale_cached_threads!(@@active_connections) do |name, conn|
|
||||
conn.disconnect!
|
||||
end
|
||||
end
|
||||
|
||||
active_connections.each_value do |connection|
|
||||
connection.verify!(@@verification_timeout)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def clear_cache!(cache, thread_id = nil, &block)
|
||||
if cache
|
||||
if @@allow_concurrency
|
||||
thread_id ||= Thread.current.object_id
|
||||
thread_cache, cache = cache, cache[thread_id]
|
||||
return unless cache
|
||||
end
|
||||
|
||||
cache.each(&block) if block_given?
|
||||
cache.clear
|
||||
end
|
||||
ensure
|
||||
if thread_cache && @@allow_concurrency
|
||||
thread_cache.delete(thread_id)
|
||||
end
|
||||
end
|
||||
|
||||
# Remove stale threads from the cache.
|
||||
def remove_stale_cached_threads!(cache, &block)
|
||||
stale = Set.new(cache.keys)
|
||||
|
||||
Thread.list.each do |thread|
|
||||
stale.delete(thread.object_id) if thread.alive?
|
||||
end
|
||||
|
||||
stale.each do |thread_id|
|
||||
clear_cache!(cache, thread_id, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def clear_all_cached_connections!
|
||||
if @@allow_concurrency
|
||||
@@active_connections.each_value do |connection_hash_for_thread|
|
||||
connection_hash_for_thread.each_value {|conn| conn.disconnect! }
|
||||
connection_hash_for_thread.clear
|
||||
end
|
||||
else
|
||||
@@active_connections.each_value {|conn| conn.disconnect! }
|
||||
end
|
||||
@@active_connections.clear
|
||||
end
|
||||
end
|
||||
# The connection handler
|
||||
cattr_accessor :connection_handler, :instance_writer => false
|
||||
@@connection_handler = ConnectionAdapters::ConnectionHandler.new
|
||||
|
||||
# Returns the connection currently associated with the class. This can
|
||||
# also be used to "borrow" the connection to do database work that isn't
|
||||
|
@ -208,9 +52,7 @@ module ActiveRecord
|
|||
raise AdapterNotSpecified unless defined? RAILS_ENV
|
||||
establish_connection(RAILS_ENV)
|
||||
when ConnectionSpecification
|
||||
clear_active_connection_name
|
||||
@active_connection_name = name
|
||||
@@defined_connections[name] = spec
|
||||
@@connection_handler.establish_connection(name, spec)
|
||||
when Symbol, String
|
||||
if configuration = configurations[spec.to_s]
|
||||
establish_connection(configuration)
|
||||
|
@ -243,67 +85,52 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Locate the connection of the nearest super class. This can be an
|
||||
# active or defined connection: if it is the latter, it will be
|
||||
# opened and set as the active connection for the class it was defined
|
||||
# for (not necessarily the current class).
|
||||
def self.retrieve_connection #:nodoc:
|
||||
# Name is nil if establish_connection hasn't been called for
|
||||
# some class along the inheritance chain up to AR::Base yet.
|
||||
if name = active_connection_name
|
||||
if conn = active_connections[name]
|
||||
# Verify the connection.
|
||||
conn.verify!(@@verification_timeout)
|
||||
elsif spec = @@defined_connections[name]
|
||||
# Activate this connection specification.
|
||||
klass = name.constantize
|
||||
klass.connection = spec
|
||||
conn = active_connections[name]
|
||||
end
|
||||
class << self
|
||||
# Deprecated and no longer has any effect.
|
||||
def allow_concurrency
|
||||
ActiveSupport::Deprecation.warn("ActiveRecord::Base.allow_concurrency has been deprecated and no longer has any effect. Please remove all references to allow_concurrency.")
|
||||
end
|
||||
|
||||
conn or raise ConnectionNotEstablished
|
||||
end
|
||||
|
||||
# Returns true if a connection that's accessible to this class has already been opened.
|
||||
def self.connected?
|
||||
active_connections[active_connection_name] ? true : false
|
||||
end
|
||||
|
||||
# Remove the connection for this class. This will close the active
|
||||
# connection and the defined connection (if they exist). The result
|
||||
# can be used as an argument for establish_connection, for easily
|
||||
# re-establishing the connection.
|
||||
def self.remove_connection(klass=self)
|
||||
spec = @@defined_connections[klass.name]
|
||||
konn = active_connections[klass.name]
|
||||
@@defined_connections.delete_if { |key, value| value == spec }
|
||||
active_connections.delete_if { |key, value| value == konn }
|
||||
konn.disconnect! if konn
|
||||
spec.config if spec
|
||||
end
|
||||
|
||||
# Set the connection for the class.
|
||||
def self.connection=(spec) #:nodoc:
|
||||
if spec.kind_of?(ActiveRecord::ConnectionAdapters::AbstractAdapter)
|
||||
active_connections[name] = spec
|
||||
elsif spec.kind_of?(ConnectionSpecification)
|
||||
config = spec.config.reverse_merge(:allow_concurrency => @@allow_concurrency)
|
||||
self.connection = self.send(spec.adapter_method, config)
|
||||
elsif spec.nil?
|
||||
raise ConnectionNotEstablished
|
||||
else
|
||||
establish_connection spec
|
||||
# Deprecated and no longer has any effect.
|
||||
def allow_concurrency=(flag)
|
||||
ActiveSupport::Deprecation.warn("ActiveRecord::Base.allow_concurrency= has been deprecated and no longer has any effect. Please remove all references to allow_concurrency=.")
|
||||
end
|
||||
end
|
||||
|
||||
# connection state logging
|
||||
def self.log_connections #:nodoc:
|
||||
if logger
|
||||
logger.info "Defined connections: #{@@defined_connections.inspect}"
|
||||
logger.info "Active connections: #{active_connections.inspect}"
|
||||
logger.info "Active connection name: #{@active_connection_name}"
|
||||
# Deprecated and no longer has any effect.
|
||||
def verification_timeout
|
||||
ActiveSupport::Deprecation.warn("ActiveRecord::Base.verification_timeout has been deprecated and no longer has any effect. Please remove all references to verification_timeout.")
|
||||
end
|
||||
|
||||
# Deprecated and no longer has any effect.
|
||||
def verification_timeout=(flag)
|
||||
ActiveSupport::Deprecation.warn("ActiveRecord::Base.verification_timeout= has been deprecated and no longer has any effect. Please remove all references to verification_timeout=.")
|
||||
end
|
||||
|
||||
# Returns the connection currently associated with the class. This can
|
||||
# also be used to "borrow" the connection to do database work unrelated
|
||||
# to any of the specific Active Records.
|
||||
def connection
|
||||
retrieve_connection
|
||||
end
|
||||
|
||||
def connection_pool
|
||||
connection_handler.retrieve_connection_pool(self)
|
||||
end
|
||||
|
||||
def retrieve_connection
|
||||
connection_handler.retrieve_connection(self)
|
||||
end
|
||||
|
||||
def connected?
|
||||
connection_handler.connected?(self)
|
||||
end
|
||||
|
||||
def remove_connection(klass = self)
|
||||
connection_handler.remove_connection(klass)
|
||||
end
|
||||
|
||||
delegate :clear_active_connections!, :clear_reloadable_connections!,
|
||||
:clear_all_connections!,:verify_active_connections!, :to => :connection_handler
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -98,8 +98,14 @@ module ActiveRecord
|
|||
add_limit_offset!(sql, options) if options
|
||||
end
|
||||
|
||||
# Appends +LIMIT+ and +OFFSET+ options to an SQL statement.
|
||||
# Appends +LIMIT+ and +OFFSET+ options to an SQL statement, or some SQL
|
||||
# fragment that has the same semantics as LIMIT and OFFSET.
|
||||
#
|
||||
# +options+ must be a Hash which contains a +:limit+ option (required)
|
||||
# and an +:offset+ option (optional).
|
||||
#
|
||||
# This method *modifies* the +sql+ parameter.
|
||||
#
|
||||
# ===== Examples
|
||||
# add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50})
|
||||
# generates
|
||||
|
@ -114,10 +120,6 @@ module ActiveRecord
|
|||
sql
|
||||
end
|
||||
|
||||
def sanitize_limit(limit)
|
||||
limit.to_s[/,/] ? limit.split(',').map{ |i| i.to_i }.join(',') : limit.to_i
|
||||
end
|
||||
|
||||
# Appends a locking clause to an SQL statement.
|
||||
# This method *modifies* the +sql+ parameter.
|
||||
# # SELECT * FROM suppliers FOR UPDATE
|
||||
|
@ -149,6 +151,14 @@ module ActiveRecord
|
|||
"INSERT INTO #{quote_table_name(table_name)} VALUES(DEFAULT)"
|
||||
end
|
||||
|
||||
def case_sensitive_equality_operator
|
||||
"="
|
||||
end
|
||||
|
||||
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
|
||||
"WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
|
||||
end
|
||||
|
||||
protected
|
||||
# Returns an array of record hashes with the column names as keys and
|
||||
# column values as values.
|
||||
|
@ -171,6 +181,21 @@ module ActiveRecord
|
|||
def delete_sql(sql, name = nil)
|
||||
update_sql(sql, name)
|
||||
end
|
||||
|
||||
# Sanitizes the given LIMIT parameter in order to prevent SQL injection.
|
||||
#
|
||||
# +limit+ may be anything that can evaluate to a string via #to_s. It
|
||||
# should look like an integer, or a comma-delimited list of integers.
|
||||
#
|
||||
# Returns the sanitized limit parameter, either as an integer, or as a
|
||||
# string which contains a comma-delimited list of integers.
|
||||
def sanitize_limit(limit)
|
||||
if limit.to_s =~ /,/
|
||||
limit.to_s.split(',').map{ |i| i.to_i }.join(',')
|
||||
else
|
||||
limit.to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,6 @@ module ActiveRecord
|
|||
class << self
|
||||
def included(base)
|
||||
base.class_eval do
|
||||
attr_accessor :query_cache_enabled
|
||||
alias_method_chain :columns, :query_cache
|
||||
alias_method_chain :select_all, :query_cache
|
||||
end
|
||||
|
@ -16,7 +15,7 @@ module ActiveRecord
|
|||
method_names.each do |method_name|
|
||||
base.class_eval <<-end_code, __FILE__, __LINE__
|
||||
def #{method_name}_with_query_dirty(*args)
|
||||
clear_query_cache if @query_cache_enabled
|
||||
clear_query_cache if query_cache_enabled
|
||||
#{method_name}_without_query_dirty(*args)
|
||||
end
|
||||
|
||||
|
@ -26,22 +25,38 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def query_cache_enabled
|
||||
Thread.current['query_cache_enabled']
|
||||
end
|
||||
|
||||
def query_cache_enabled=(flag)
|
||||
Thread.current['query_cache_enabled'] = flag
|
||||
end
|
||||
|
||||
def query_cache
|
||||
Thread.current['query_cache']
|
||||
end
|
||||
|
||||
def query_cache=(cache)
|
||||
Thread.current['query_cache'] = cache
|
||||
end
|
||||
|
||||
# Enable the query cache within the block.
|
||||
def cache
|
||||
old, @query_cache_enabled = @query_cache_enabled, true
|
||||
@query_cache ||= {}
|
||||
old, self.query_cache_enabled = query_cache_enabled, true
|
||||
self.query_cache ||= {}
|
||||
yield
|
||||
ensure
|
||||
clear_query_cache
|
||||
@query_cache_enabled = old
|
||||
self.query_cache_enabled = old
|
||||
end
|
||||
|
||||
# Disable the query cache within the block.
|
||||
def uncached
|
||||
old, @query_cache_enabled = @query_cache_enabled, false
|
||||
old, self.query_cache_enabled = query_cache_enabled, false
|
||||
yield
|
||||
ensure
|
||||
@query_cache_enabled = old
|
||||
self.query_cache_enabled = old
|
||||
end
|
||||
|
||||
# Clears the query cache.
|
||||
|
@ -51,11 +66,11 @@ module ActiveRecord
|
|||
# the same SQL query and repeatedly return the same result each time, silently
|
||||
# undermining the randomness you were expecting.
|
||||
def clear_query_cache
|
||||
@query_cache.clear if @query_cache
|
||||
query_cache.clear if query_cache
|
||||
end
|
||||
|
||||
def select_all_with_query_cache(*args)
|
||||
if @query_cache_enabled
|
||||
if query_cache_enabled
|
||||
cache_sql(args.first) { select_all_without_query_cache(*args) }
|
||||
else
|
||||
select_all_without_query_cache(*args)
|
||||
|
@ -63,8 +78,8 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def columns_with_query_cache(*args)
|
||||
if @query_cache_enabled
|
||||
@query_cache["SHOW FIELDS FROM #{args.first}"] ||= columns_without_query_cache(*args)
|
||||
if query_cache_enabled
|
||||
query_cache["SHOW FIELDS FROM #{args.first}"] ||= columns_without_query_cache(*args)
|
||||
else
|
||||
columns_without_query_cache(*args)
|
||||
end
|
||||
|
@ -73,11 +88,11 @@ module ActiveRecord
|
|||
private
|
||||
def cache_sql(sql)
|
||||
result =
|
||||
if @query_cache.has_key?(sql)
|
||||
if query_cache.has_key?(sql)
|
||||
log_info(sql, "CACHE", 0.0)
|
||||
@query_cache[sql]
|
||||
query_cache[sql]
|
||||
else
|
||||
@query_cache[sql] = yield
|
||||
query_cache[sql] = yield
|
||||
end
|
||||
|
||||
if Array === result
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
require 'date'
|
||||
require 'set'
|
||||
require 'bigdecimal'
|
||||
require 'bigdecimal/util'
|
||||
|
||||
|
@ -6,6 +7,8 @@ module ActiveRecord
|
|||
module ConnectionAdapters #:nodoc:
|
||||
# An abstract definition of a column in a table.
|
||||
class Column
|
||||
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
|
||||
|
||||
module Format
|
||||
ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
|
||||
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
|
||||
|
@ -30,11 +33,15 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def text?
|
||||
[:string, :text].include? type
|
||||
type == :string || type == :text
|
||||
end
|
||||
|
||||
def number?
|
||||
[:float, :integer, :decimal].include? type
|
||||
type == :integer || type == :float || type == :decimal
|
||||
end
|
||||
|
||||
def has_default?
|
||||
!default.nil?
|
||||
end
|
||||
|
||||
# Returns the Ruby class that corresponds to the abstract data type.
|
||||
|
@ -135,10 +142,10 @@ module ActiveRecord
|
|||
|
||||
# convert something to a boolean
|
||||
def value_to_boolean(value)
|
||||
if value == true || value == false
|
||||
value
|
||||
if value.is_a?(String) && value.blank?
|
||||
nil
|
||||
else
|
||||
%w(true t 1).include?(value.to_s.downcase)
|
||||
TRUE_VALUES.include?(value)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -249,6 +256,10 @@ module ActiveRecord
|
|||
class IndexDefinition < Struct.new(:table, :name, :unique, :columns) #:nodoc:
|
||||
end
|
||||
|
||||
# Abstract representation of a column definition. Instances of this type
|
||||
# are typically created by methods in TableDefinition, and added to the
|
||||
# +columns+ attribute of said TableDefinition object, in order to be used
|
||||
# for generating a number of table creation or table changing SQL statements.
|
||||
class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc:
|
||||
|
||||
def sql_type
|
||||
|
@ -272,9 +283,29 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Represents a SQL table in an abstract way.
|
||||
# Columns are stored as a ColumnDefinition in the +columns+ attribute.
|
||||
# Represents the schema of an SQL table in an abstract way. This class
|
||||
# provides methods for manipulating the schema representation.
|
||||
#
|
||||
# Inside migration files, the +t+ object in +create_table+ and
|
||||
# +change_table+ is actually of this type:
|
||||
#
|
||||
# class SomeMigration < ActiveRecord::Migration
|
||||
# def self.up
|
||||
# create_table :foo do |t|
|
||||
# puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def self.down
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# The table definitions
|
||||
# The Columns are stored as a ColumnDefinition in the +columns+ attribute.
|
||||
class TableDefinition
|
||||
# An array of ColumnDefinition objects, representing the column changes
|
||||
# that have been defined.
|
||||
attr_accessor :columns
|
||||
|
||||
def initialize(base)
|
||||
|
@ -318,6 +349,12 @@ module ActiveRecord
|
|||
# * <tt>:scale</tt> -
|
||||
# Specifies the scale for a <tt>:decimal</tt> column.
|
||||
#
|
||||
# For clarity's sake: the precision is the number of significant digits,
|
||||
# while the scale is the number of digits that can be stored following
|
||||
# the decimal point. For example, the number 123.45 has a precision of 5
|
||||
# and a scale of 2. A decimal with a precision of 5 and a scale of 2 can
|
||||
# range from -999.99 to 999.99.
|
||||
#
|
||||
# Please be aware of different RDBMS implementations behavior with
|
||||
# <tt>:decimal</tt> columns:
|
||||
# * The SQL standard says the default scale should be 0, <tt>:scale</tt> <=
|
||||
|
@ -371,6 +408,10 @@ module ActiveRecord
|
|||
# td.column(:huge_integer, :decimal, :precision => 30)
|
||||
# # => huge_integer DECIMAL(30)
|
||||
#
|
||||
# # Defines a column with a database-specific type.
|
||||
# td.column(:foo, 'polygon')
|
||||
# # => foo polygon
|
||||
#
|
||||
# == Short-hand examples
|
||||
#
|
||||
# Instead of calling +column+ directly, you can also work with the short-hand definitions for the default types.
|
||||
|
|
|
@ -31,19 +31,25 @@ module ActiveRecord
|
|||
# See the concrete implementation for details on the expected parameter values.
|
||||
def columns(table_name, name = nil) end
|
||||
|
||||
# Creates a new table
|
||||
# Creates a new table with the name +table_name+. +table_name+ may either
|
||||
# be a String or a Symbol.
|
||||
#
|
||||
# There are two ways to work with +create_table+. You can use the block
|
||||
# form or the regular form, like this:
|
||||
#
|
||||
# === Block form
|
||||
# # create_table() yields a TableDefinition instance
|
||||
# # create_table() passes a TableDefinition object to the block.
|
||||
# # This form will not only create the table, but also columns for the
|
||||
# # table.
|
||||
# create_table(:suppliers) do |t|
|
||||
# t.column :name, :string, :limit => 60
|
||||
# # Other fields here
|
||||
# end
|
||||
#
|
||||
# === Regular form
|
||||
# # Creates a table called 'suppliers' with no columns.
|
||||
# create_table(:suppliers)
|
||||
# # Add a column to 'suppliers'.
|
||||
# add_column(:suppliers, :name, :string, {:limit => 60})
|
||||
#
|
||||
# The +options+ hash can include the following keys:
|
||||
|
@ -356,7 +362,7 @@ module ActiveRecord
|
|||
|
||||
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
|
||||
if native = native_database_types[type]
|
||||
column_type_sql = native.is_a?(Hash) ? native[:name] : native
|
||||
column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup
|
||||
|
||||
if type == :decimal # ignore limit, use precision and scale
|
||||
scale ||= native[:scale]
|
||||
|
@ -371,7 +377,7 @@ module ActiveRecord
|
|||
raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale if specified"
|
||||
end
|
||||
|
||||
elsif limit ||= native.is_a?(Hash) && native[:limit]
|
||||
elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit])
|
||||
column_type_sql << "(#{limit})"
|
||||
end
|
||||
|
||||
|
|
|
@ -7,23 +7,31 @@ require 'active_record/connection_adapters/abstract/schema_definitions'
|
|||
require 'active_record/connection_adapters/abstract/schema_statements'
|
||||
require 'active_record/connection_adapters/abstract/database_statements'
|
||||
require 'active_record/connection_adapters/abstract/quoting'
|
||||
require 'active_record/connection_adapters/abstract/connection_pool'
|
||||
require 'active_record/connection_adapters/abstract/connection_specification'
|
||||
require 'active_record/connection_adapters/abstract/query_cache'
|
||||
|
||||
module ActiveRecord
|
||||
module ConnectionAdapters # :nodoc:
|
||||
# All the concrete database adapters follow the interface laid down in this class.
|
||||
# You can use this interface directly by borrowing the database connection from the Base with
|
||||
# Base.connection.
|
||||
# ActiveRecord supports multiple database systems. AbstractAdapter and
|
||||
# related classes form the abstraction layer which makes this possible.
|
||||
# An AbstractAdapter represents a connection to a database, and provides an
|
||||
# abstract interface for database-specific functionality such as establishing
|
||||
# a connection, escaping values, building the right SQL fragments for ':offset'
|
||||
# and ':limit' options, etc.
|
||||
#
|
||||
# Most of the methods in the adapter are useful during migrations. Most
|
||||
# notably, SchemaStatements#create_table, SchemaStatements#drop_table,
|
||||
# SchemaStatements#add_index, SchemaStatements#remove_index,
|
||||
# SchemaStatements#add_column, SchemaStatements#change_column and
|
||||
# SchemaStatements#remove_column are very useful.
|
||||
# All the concrete database adapters follow the interface laid down in this class.
|
||||
# ActiveRecord::Base.connection returns an AbstractAdapter object, which
|
||||
# you can use.
|
||||
#
|
||||
# Most of the methods in the adapter are useful during migrations. Most
|
||||
# notably, the instance methods provided by SchemaStatement are very useful.
|
||||
class AbstractAdapter
|
||||
include Quoting, DatabaseStatements, SchemaStatements
|
||||
include QueryCache
|
||||
include ActiveSupport::Callbacks
|
||||
define_callbacks :checkout, :checkin
|
||||
checkout :reset!
|
||||
@@row_even = true
|
||||
|
||||
def initialize(connection, logger = nil) #:nodoc:
|
||||
|
@ -51,6 +59,13 @@ module ActiveRecord
|
|||
true
|
||||
end
|
||||
|
||||
# Does this adapter support DDL rollbacks in transactions? That is, would
|
||||
# CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL,
|
||||
# SQL Server, and others support this. MySQL and others do not.
|
||||
def supports_ddl_transactions?
|
||||
false
|
||||
end
|
||||
|
||||
# Should primary key values be selected from their corresponding
|
||||
# sequence before the insert statement? If true, next_sequence_value
|
||||
# is called before each insert to set the record's primary key.
|
||||
|
@ -80,48 +95,75 @@ module ActiveRecord
|
|||
|
||||
# CONNECTION MANAGEMENT ====================================
|
||||
|
||||
# Is this connection active and ready to perform queries?
|
||||
# Checks whether the connection to the database is still active. This includes
|
||||
# checking whether the database is actually capable of responding, i.e. whether
|
||||
# the connection isn't stale.
|
||||
def active?
|
||||
@active != false
|
||||
end
|
||||
|
||||
# Close this connection and open a new one in its place.
|
||||
# Disconnects from the database if already connected, and establishes a
|
||||
# new connection with the database.
|
||||
def reconnect!
|
||||
@active = true
|
||||
end
|
||||
|
||||
# Close this connection
|
||||
# Disconnects from the database if already connected. Otherwise, this
|
||||
# method does nothing.
|
||||
def disconnect!
|
||||
@active = false
|
||||
end
|
||||
|
||||
# Reset the state of this connection, directing the DBMS to clear
|
||||
# transactions and other connection-related server-side state. Usually a
|
||||
# database-dependent operation.
|
||||
#
|
||||
# The default implementation does nothing; the implementation should be
|
||||
# overridden by concrete adapters.
|
||||
def reset!
|
||||
# this should be overridden by concrete adapters
|
||||
end
|
||||
|
||||
# Returns true if its safe to reload the connection between requests for development mode.
|
||||
# This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite.
|
||||
def requires_reloading?
|
||||
false
|
||||
end
|
||||
|
||||
# Lazily verify this connection, calling <tt>active?</tt> only if it hasn't
|
||||
# been called for +timeout+ seconds.
|
||||
def verify!(timeout)
|
||||
now = Time.now.to_i
|
||||
if (now - @last_verification) > timeout
|
||||
reconnect! unless active?
|
||||
@last_verification = now
|
||||
end
|
||||
# Checks whether the connection to the database is still active (i.e. not stale).
|
||||
# This is done under the hood by calling <tt>active?</tt>. If the connection
|
||||
# is no longer active, then this method will reconnect to the database.
|
||||
def verify!(*ignored)
|
||||
reconnect! unless active?
|
||||
end
|
||||
|
||||
# Provides access to the underlying database connection. Useful for
|
||||
# when you need to call a proprietary method such as postgresql's lo_*
|
||||
# methods
|
||||
# Provides access to the underlying database driver for this adapter. For
|
||||
# example, this method returns a Mysql object in case of MysqlAdapter,
|
||||
# and a PGconn object in case of PostgreSQLAdapter.
|
||||
#
|
||||
# This is useful for when you need to call a proprietary method such as
|
||||
# PostgreSQL's lo_* methods.
|
||||
def raw_connection
|
||||
@connection
|
||||
end
|
||||
|
||||
def log_info(sql, name, runtime)
|
||||
def open_transactions
|
||||
@open_transactions ||= 0
|
||||
end
|
||||
|
||||
def increment_open_transactions
|
||||
@open_transactions ||= 0
|
||||
@open_transactions += 1
|
||||
end
|
||||
|
||||
def decrement_open_transactions
|
||||
@open_transactions -= 1
|
||||
end
|
||||
|
||||
def log_info(sql, name, seconds)
|
||||
if @logger && @logger.debug?
|
||||
name = "#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})"
|
||||
@logger.debug format_log_entry(name, sql.squeeze(' '))
|
||||
name = "#{name.nil? ? "SQL" : name} (#{sprintf("%.1f", seconds * 1000)}ms)"
|
||||
@logger.debug(format_log_entry(name, sql.squeeze(' ')))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -42,27 +42,6 @@ end
|
|||
|
||||
module ActiveRecord
|
||||
class Base
|
||||
def self.require_mysql
|
||||
# Include the MySQL driver if one hasn't already been loaded
|
||||
unless defined? Mysql
|
||||
begin
|
||||
require_library_or_gem 'mysql'
|
||||
rescue LoadError => cannot_require_mysql
|
||||
# Use the bundled Ruby/MySQL driver if no driver is already in place
|
||||
begin
|
||||
ActiveSupport::Deprecation.warn "You're using the Ruby-based MySQL library that ships with Rails. This library will be REMOVED FROM RAILS 2.2. Please switch to the offical mysql gem: `gem install mysql`", caller
|
||||
|
||||
require 'active_record/vendor/mysql'
|
||||
rescue LoadError
|
||||
raise cannot_require_mysql
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Define Mysql::Result.all_hashes
|
||||
MysqlCompat.define_all_hashes_method!
|
||||
end
|
||||
|
||||
# Establishes a connection to the database that's used by all Active Record objects.
|
||||
def self.mysql_connection(config) # :nodoc:
|
||||
config = config.symbolize_keys
|
||||
|
@ -78,9 +57,19 @@ module ActiveRecord
|
|||
raise ArgumentError, "No database specified. Missing argument: database."
|
||||
end
|
||||
|
||||
require_mysql
|
||||
# Require the MySQL driver and define Mysql::Result.all_hashes
|
||||
unless defined? Mysql
|
||||
begin
|
||||
require_library_or_gem('mysql')
|
||||
rescue LoadError
|
||||
$stderr.puts '!!! The bundled mysql.rb driver has been removed from Rails 2.2. Please install the mysql gem and try again: gem install mysql.'
|
||||
raise
|
||||
end
|
||||
end
|
||||
MysqlCompat.define_all_hashes_method!
|
||||
|
||||
mysql = Mysql.init
|
||||
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
|
||||
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
|
||||
|
||||
ConnectionAdapters::MysqlAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
|
||||
end
|
||||
|
@ -91,7 +80,7 @@ module ActiveRecord
|
|||
def extract_default(default)
|
||||
if type == :binary || type == :text
|
||||
if default.blank?
|
||||
nil
|
||||
return null ? nil : ''
|
||||
else
|
||||
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
|
||||
end
|
||||
|
@ -102,6 +91,11 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def has_default?
|
||||
return false if type == :binary || type == :text #mysql forbids defaults on blob and text columns
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
def simplified_type(field_type)
|
||||
return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
|
||||
|
@ -156,6 +150,7 @@ module ActiveRecord
|
|||
# * <tt>:password</tt> - Defaults to nothing.
|
||||
# * <tt>:database</tt> - The name of the database. No default, must be provided.
|
||||
# * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection.
|
||||
# * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
|
||||
# * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
|
||||
# * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
|
||||
# * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection.
|
||||
|
@ -168,8 +163,10 @@ module ActiveRecord
|
|||
#
|
||||
# ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
|
||||
class MysqlAdapter < AbstractAdapter
|
||||
@@emulate_booleans = true
|
||||
cattr_accessor :emulate_booleans
|
||||
self.emulate_booleans = true
|
||||
|
||||
ADAPTER_NAME = 'MySQL'.freeze
|
||||
|
||||
LOST_CONNECTION_ERROR_MESSAGES = [
|
||||
"Server shutdown in progress",
|
||||
|
@ -177,7 +174,22 @@ module ActiveRecord
|
|||
"Lost connection to MySQL server during query",
|
||||
"MySQL server has gone away" ]
|
||||
|
||||
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
|
||||
QUOTED_TRUE, QUOTED_FALSE = '1'.freeze, '0'.freeze
|
||||
|
||||
NATIVE_DATABASE_TYPES = {
|
||||
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze,
|
||||
:string => { :name => "varchar", :limit => 255 },
|
||||
:text => { :name => "text" },
|
||||
:integer => { :name => "int", :limit => 4 },
|
||||
:float => { :name => "float" },
|
||||
:decimal => { :name => "decimal" },
|
||||
:datetime => { :name => "datetime" },
|
||||
:timestamp => { :name => "datetime" },
|
||||
:time => { :name => "time" },
|
||||
:date => { :name => "date" },
|
||||
:binary => { :name => "blob" },
|
||||
:boolean => { :name => "tinyint", :limit => 1 }
|
||||
}
|
||||
|
||||
def initialize(connection, logger, connection_options, config)
|
||||
super(connection, logger)
|
||||
|
@ -187,7 +199,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def adapter_name #:nodoc:
|
||||
'MySQL'
|
||||
ADAPTER_NAME
|
||||
end
|
||||
|
||||
def supports_migrations? #:nodoc:
|
||||
|
@ -195,20 +207,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def native_database_types #:nodoc:
|
||||
{
|
||||
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze,
|
||||
:string => { :name => "varchar", :limit => 255 },
|
||||
:text => { :name => "text" },
|
||||
:integer => { :name => "int", :limit => 4 },
|
||||
:float => { :name => "float" },
|
||||
:decimal => { :name => "decimal" },
|
||||
:datetime => { :name => "datetime" },
|
||||
:timestamp => { :name => "datetime" },
|
||||
:time => { :name => "time" },
|
||||
:date => { :name => "date" },
|
||||
:binary => { :name => "blob" },
|
||||
:boolean => { :name => "tinyint", :limit => 1 }
|
||||
}
|
||||
NATIVE_DATABASE_TYPES
|
||||
end
|
||||
|
||||
|
||||
|
@ -219,7 +218,7 @@ module ActiveRecord
|
|||
s = column.class.string_to_binary(value).unpack("H*")[0]
|
||||
"x'#{s}'"
|
||||
elsif value.kind_of?(BigDecimal)
|
||||
"'#{value.to_s("F")}'"
|
||||
value.to_s("F")
|
||||
else
|
||||
super
|
||||
end
|
||||
|
@ -286,6 +285,14 @@ module ActiveRecord
|
|||
@connection.close rescue nil
|
||||
end
|
||||
|
||||
def reset!
|
||||
if @connection.respond_to?(:change_user)
|
||||
# See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to
|
||||
# reset the connection is to change the user to the same user.
|
||||
@connection.change_user(@config[:username], @config[:password], @config[:database])
|
||||
configure_connection
|
||||
end
|
||||
end
|
||||
|
||||
# DATABASE STATEMENTS ======================================
|
||||
|
||||
|
@ -364,9 +371,9 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def recreate_database(name) #:nodoc:
|
||||
def recreate_database(name, options = {}) #:nodoc:
|
||||
drop_database(name)
|
||||
create_database(name)
|
||||
create_database(name, options)
|
||||
end
|
||||
|
||||
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
|
||||
|
@ -517,14 +524,33 @@ module ActiveRecord
|
|||
keys.length == 1 ? [keys.first, nil] : nil
|
||||
end
|
||||
|
||||
def case_sensitive_equality_operator
|
||||
"= BINARY"
|
||||
end
|
||||
|
||||
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
|
||||
where_sql
|
||||
end
|
||||
|
||||
private
|
||||
def connect
|
||||
@connection.reconnect = true if @connection.respond_to?(:reconnect=)
|
||||
|
||||
encoding = @config[:encoding]
|
||||
if encoding
|
||||
@connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
|
||||
end
|
||||
@connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
|
||||
|
||||
if @config[:sslca] || @config[:sslkey]
|
||||
@connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher])
|
||||
end
|
||||
|
||||
@connection.real_connect(*@connection_options)
|
||||
configure_connection
|
||||
end
|
||||
|
||||
def configure_connection
|
||||
encoding = @config[:encoding]
|
||||
execute("SET NAMES '#{encoding}'") if encoding
|
||||
|
||||
# By default, MySQL 'where id is null' selects the last inserted id.
|
||||
|
|
|
@ -246,9 +246,26 @@ module ActiveRecord
|
|||
# * <tt>:min_messages</tt> - An optional client min messages that is used in a <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
|
||||
# * <tt>:allow_concurrency</tt> - If true, use async query methods so Ruby threads don't deadlock; otherwise, use blocking query methods.
|
||||
class PostgreSQLAdapter < AbstractAdapter
|
||||
ADAPTER_NAME = 'PostgreSQL'.freeze
|
||||
|
||||
NATIVE_DATABASE_TYPES = {
|
||||
:primary_key => "serial primary key".freeze,
|
||||
:string => { :name => "character varying", :limit => 255 },
|
||||
:text => { :name => "text" },
|
||||
:integer => { :name => "integer" },
|
||||
:float => { :name => "float" },
|
||||
:decimal => { :name => "decimal" },
|
||||
:datetime => { :name => "timestamp" },
|
||||
:timestamp => { :name => "timestamp" },
|
||||
:time => { :name => "time" },
|
||||
:date => { :name => "date" },
|
||||
:binary => { :name => "bytea" },
|
||||
:boolean => { :name => "boolean" }
|
||||
}
|
||||
|
||||
# Returns 'PostgreSQL' as adapter name for identification purposes.
|
||||
def adapter_name
|
||||
'PostgreSQL'
|
||||
ADAPTER_NAME
|
||||
end
|
||||
|
||||
# Initializes and connects a PostgreSQL adapter.
|
||||
|
@ -290,20 +307,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def native_database_types #:nodoc:
|
||||
{
|
||||
:primary_key => "serial primary key",
|
||||
:string => { :name => "character varying", :limit => 255 },
|
||||
:text => { :name => "text" },
|
||||
:integer => { :name => "integer" },
|
||||
:float => { :name => "float" },
|
||||
:decimal => { :name => "decimal" },
|
||||
:datetime => { :name => "timestamp" },
|
||||
:timestamp => { :name => "timestamp" },
|
||||
:time => { :name => "time" },
|
||||
:date => { :name => "date" },
|
||||
:binary => { :name => "bytea" },
|
||||
:boolean => { :name => "boolean" }
|
||||
}
|
||||
NATIVE_DATABASE_TYPES
|
||||
end
|
||||
|
||||
# Does PostgreSQL support migrations?
|
||||
|
@ -331,6 +335,10 @@ module ActiveRecord
|
|||
postgresql_version >= 80200
|
||||
end
|
||||
|
||||
def supports_ddl_transactions?
|
||||
true
|
||||
end
|
||||
|
||||
# Returns the configured supported identifier length supported by PostgreSQL,
|
||||
# or report the default of 63 on PostgreSQL 7.x.
|
||||
def table_alias_length
|
||||
|
@ -510,6 +518,45 @@ module ActiveRecord
|
|||
execute "ROLLBACK"
|
||||
end
|
||||
|
||||
# ruby-pg defines Ruby constants for transaction status,
|
||||
# ruby-postgres does not.
|
||||
PQTRANS_IDLE = defined?(PGconn::PQTRANS_IDLE) ? PGconn::PQTRANS_IDLE : 0
|
||||
|
||||
# Check whether a transaction is active.
|
||||
def transaction_active?
|
||||
@connection.transaction_status != PQTRANS_IDLE
|
||||
end
|
||||
|
||||
# Wrap a block in a transaction. Returns result of block.
|
||||
def transaction(start_db_transaction = true)
|
||||
transaction_open = false
|
||||
begin
|
||||
if block_given?
|
||||
if start_db_transaction
|
||||
begin_db_transaction
|
||||
transaction_open = true
|
||||
end
|
||||
yield
|
||||
end
|
||||
rescue Exception => database_transaction_rollback
|
||||
if transaction_open && transaction_active?
|
||||
transaction_open = false
|
||||
rollback_db_transaction
|
||||
end
|
||||
raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
|
||||
end
|
||||
ensure
|
||||
if transaction_open && transaction_active?
|
||||
begin
|
||||
commit_db_transaction
|
||||
rescue Exception => database_transaction_rollback
|
||||
rollback_db_transaction
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# SCHEMA STATEMENTS ========================================
|
||||
|
||||
def recreate_database(name) #:nodoc:
|
||||
|
@ -618,6 +665,19 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Returns the current database name.
|
||||
def current_database
|
||||
query('select current_database()')[0][0]
|
||||
end
|
||||
|
||||
# Returns the current database encoding format.
|
||||
def encoding
|
||||
query(<<-end_sql)[0][0]
|
||||
SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database
|
||||
WHERE pg_database.datname LIKE '#{current_database}'
|
||||
end_sql
|
||||
end
|
||||
|
||||
# Sets the schema search path to a string of comma-separated schema names.
|
||||
# Names beginning with $ have to be quoted (e.g. $user => '$user').
|
||||
# See: http://www.postgresql.org/docs/current/static/ddl-schemas.html
|
||||
|
@ -851,7 +911,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
private
|
||||
# The internal PostgreSQL identifer of the money data type.
|
||||
# The internal PostgreSQL identifier of the money data type.
|
||||
MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
|
||||
|
||||
# Connects to a PostgreSQL server and sets up the adapter depending on the
|
||||
|
|
|
@ -34,8 +34,10 @@ module ActiveRecord
|
|||
# person.name << 'by'
|
||||
# person.name_change # => ['uncle bob', 'uncle bobby']
|
||||
module Dirty
|
||||
DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']
|
||||
|
||||
def self.included(base)
|
||||
base.attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
|
||||
base.attribute_method_suffix *DIRTY_SUFFIXES
|
||||
base.alias_method_chain :write_attribute, :dirty
|
||||
base.alias_method_chain :save, :dirty
|
||||
base.alias_method_chain :save!, :dirty
|
||||
|
@ -44,6 +46,8 @@ module ActiveRecord
|
|||
|
||||
base.superclass_delegating_accessor :partial_updates
|
||||
base.partial_updates = true
|
||||
|
||||
base.send(:extend, ClassMethods)
|
||||
end
|
||||
|
||||
# Do any attributes have unsaved changes?
|
||||
|
@ -62,7 +66,7 @@ module ActiveRecord
|
|||
changed_attributes.keys
|
||||
end
|
||||
|
||||
# Map of changed attrs => [original value, new value]
|
||||
# Map of changed attrs => [original value, new value].
|
||||
# person.changes # => {}
|
||||
# person.name = 'bob'
|
||||
# person.changes # => { 'name' => ['bill', 'bob'] }
|
||||
|
@ -93,27 +97,27 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
private
|
||||
# Map of change attr => original value.
|
||||
# Map of change <tt>attr => original value</tt>.
|
||||
def changed_attributes
|
||||
@changed_attributes ||= {}
|
||||
end
|
||||
|
||||
# Handle *_changed? for method_missing.
|
||||
# Handle <tt>*_changed?</tt> for +method_missing+.
|
||||
def attribute_changed?(attr)
|
||||
changed_attributes.include?(attr)
|
||||
end
|
||||
|
||||
# Handle *_change for method_missing.
|
||||
# Handle <tt>*_change</tt> for +method_missing+.
|
||||
def attribute_change(attr)
|
||||
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
|
||||
end
|
||||
|
||||
# Handle *_was for method_missing.
|
||||
# Handle <tt>*_was</tt> for +method_missing+.
|
||||
def attribute_was(attr)
|
||||
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
|
||||
end
|
||||
|
||||
# Handle *_will_change! for method_missing.
|
||||
# Handle <tt>*_will_change!</tt> for +method_missing+.
|
||||
def attribute_will_change!(attr)
|
||||
changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
|
||||
end
|
||||
|
@ -161,5 +165,19 @@ module ActiveRecord
|
|||
old != value
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def self.extended(base)
|
||||
base.metaclass.alias_method_chain(:alias_attribute, :dirty)
|
||||
end
|
||||
|
||||
def alias_attribute_with_dirty(new_name, old_name)
|
||||
alias_attribute_without_dirty(new_name, old_name)
|
||||
DIRTY_SUFFIXES.each do |suffix|
|
||||
module_eval <<-STR, __FILE__, __LINE__+1
|
||||
def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end
|
||||
STR
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
41
vendor/rails/activerecord/lib/active_record/dynamic_finder_match.rb
vendored
Normal file
41
vendor/rails/activerecord/lib/active_record/dynamic_finder_match.rb
vendored
Normal file
|
@ -0,0 +1,41 @@
|
|||
module ActiveRecord
|
||||
class DynamicFinderMatch
|
||||
def self.match(method)
|
||||
df_match = self.new(method)
|
||||
df_match.finder ? df_match : nil
|
||||
end
|
||||
|
||||
def initialize(method)
|
||||
@finder = :first
|
||||
case method.to_s
|
||||
when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
|
||||
@finder = :last if $1 == 'last_by'
|
||||
@finder = :all if $1 == 'all_by'
|
||||
names = $2
|
||||
when /^find_by_([_a-zA-Z]\w*)\!$/
|
||||
@bang = true
|
||||
names = $1
|
||||
when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
|
||||
@instantiator = $1 == 'initialize' ? :new : :create
|
||||
names = $2
|
||||
else
|
||||
@finder = nil
|
||||
end
|
||||
@attribute_names = names && names.split('_and_')
|
||||
end
|
||||
|
||||
attr_reader :finder, :attribute_names, :instantiator
|
||||
|
||||
def finder?
|
||||
!@finder.nil? && @instantiator.nil?
|
||||
end
|
||||
|
||||
def instantiator?
|
||||
@finder == :first && !@instantiator.nil?
|
||||
end
|
||||
|
||||
def bang?
|
||||
@bang
|
||||
end
|
||||
end
|
||||
end
|
|
@ -515,7 +515,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
|
|||
|
||||
all_loaded_fixtures.update(fixtures_map)
|
||||
|
||||
connection.transaction(Thread.current['open_transactions'].to_i == 0) do
|
||||
connection.transaction(connection.open_transactions.zero?) do
|
||||
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
|
||||
fixtures.each { |fixture| fixture.insert_fixtures }
|
||||
|
||||
|
@ -541,13 +541,14 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
|
|||
label.to_s.hash.abs
|
||||
end
|
||||
|
||||
attr_reader :table_name
|
||||
attr_reader :table_name, :name
|
||||
|
||||
def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
|
||||
@connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
|
||||
@name = table_name # preserve fixture base name
|
||||
@class_name = class_name ||
|
||||
(ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
|
||||
@table_name = ActiveRecord::Base.table_name_prefix + @table_name + ActiveRecord::Base.table_name_suffix
|
||||
@table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}"
|
||||
@table_name = class_name.table_name if class_name.respond_to?(:table_name)
|
||||
@connection = class_name.connection if class_name.respond_to?(:connection)
|
||||
read_fixture_files
|
||||
|
@ -929,7 +930,7 @@ module Test #:nodoc:
|
|||
load_fixtures
|
||||
@@already_loaded_fixtures[self.class] = @loaded_fixtures
|
||||
end
|
||||
ActiveRecord::Base.send :increment_open_transactions
|
||||
ActiveRecord::Base.connection.increment_open_transactions
|
||||
ActiveRecord::Base.connection.begin_db_transaction
|
||||
# Load fixtures for every test.
|
||||
else
|
||||
|
@ -950,11 +951,11 @@ module Test #:nodoc:
|
|||
end
|
||||
|
||||
# Rollback changes if a transaction is active.
|
||||
if use_transactional_fixtures? && Thread.current['open_transactions'] != 0
|
||||
if use_transactional_fixtures? && ActiveRecord::Base.connection.open_transactions != 0
|
||||
ActiveRecord::Base.connection.rollback_db_transaction
|
||||
Thread.current['open_transactions'] = 0
|
||||
ActiveRecord::Base.connection.decrement_open_transactions
|
||||
end
|
||||
ActiveRecord::Base.verify_active_connections!
|
||||
ActiveRecord::Base.clear_active_connections!
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -963,9 +964,9 @@ module Test #:nodoc:
|
|||
fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
|
||||
unless fixtures.nil?
|
||||
if fixtures.instance_of?(Fixtures)
|
||||
@loaded_fixtures[fixtures.table_name] = fixtures
|
||||
@loaded_fixtures[fixtures.name] = fixtures
|
||||
else
|
||||
fixtures.each { |f| @loaded_fixtures[f.table_name] = f }
|
||||
fixtures.each { |f| @loaded_fixtures[f.name] = f }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
26
vendor/rails/activerecord/lib/active_record/i18n_interpolation_deprecation.rb
vendored
Normal file
26
vendor/rails/activerecord/lib/active_record/i18n_interpolation_deprecation.rb
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Deprecates the use of the former message interpolation syntax in activerecord
|
||||
# as in "must have %d characters". The new syntax uses explicit variable names
|
||||
# as in "{{value}} must have {{count}} characters".
|
||||
|
||||
require 'i18n/backend/simple'
|
||||
module I18n
|
||||
module Backend
|
||||
class Simple
|
||||
DEPRECATED_INTERPOLATORS = { '%d' => '{{count}}', '%s' => '{{value}}' }
|
||||
|
||||
protected
|
||||
def interpolate_with_deprecated_syntax(locale, string, values = {})
|
||||
return string unless string.is_a?(String)
|
||||
|
||||
string = string.gsub(/%d|%s/) do |s|
|
||||
instead = DEPRECATED_INTERPOLATORS[s]
|
||||
ActiveSupport::Deprecation.warn "using #{s} in messages is deprecated; use #{instead} instead."
|
||||
instead
|
||||
end
|
||||
|
||||
interpolate_without_deprecated_syntax(locale, string, values)
|
||||
end
|
||||
alias_method_chain :interpolate, :deprecated_syntax
|
||||
end
|
||||
end
|
||||
end
|
54
vendor/rails/activerecord/lib/active_record/locale/en-US.yml
vendored
Normal file
54
vendor/rails/activerecord/lib/active_record/locale/en-US.yml
vendored
Normal file
|
@ -0,0 +1,54 @@
|
|||
en-US:
|
||||
activerecord:
|
||||
errors:
|
||||
# The values :model, :attribute and :value are always available for interpolation
|
||||
# The value :count is available when applicable. Can be used for pluralization.
|
||||
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 {{count}} characters)"
|
||||
too_short: "is too short (minimum is {{count}} characters)"
|
||||
wrong_length: "is the wrong length (should be {{count}} characters)"
|
||||
taken: "has already been taken"
|
||||
not_a_number: "is not a number"
|
||||
greater_than: "must be greater than {{count}}"
|
||||
greater_than_or_equal_to: "must be greater than or equal to {{count}}"
|
||||
equal_to: "must be equal to {{count}}"
|
||||
less_than: "must be less than {{count}}"
|
||||
less_than_or_equal_to: "must be less than or equal to {{count}}"
|
||||
odd: "must be odd"
|
||||
even: "must be even"
|
||||
# Append your own errors here or at the model/attributes scope.
|
||||
|
||||
# You can define own errors for models or model attributes.
|
||||
# The values :model, :attribute and :value are always available for interpolation.
|
||||
#
|
||||
# For example,
|
||||
# models:
|
||||
# user:
|
||||
# blank: "This is a custom blank message for {{model}}: {{attribute}}"
|
||||
# attributes:
|
||||
# login:
|
||||
# blank: "This is a custom blank message for User login"
|
||||
# Will define custom blank validation message for User model and
|
||||
# custom blank validation message for login attribute of User model.
|
||||
models:
|
||||
|
||||
# Translate model names. Used in Model.human_name().
|
||||
#models:
|
||||
# For example,
|
||||
# user: "Dude"
|
||||
# will translate User model name to "Dude"
|
||||
|
||||
# Translate model attribute names. Used in Model.human_attribute_name(attribute).
|
||||
#attributes:
|
||||
# For example,
|
||||
# user:
|
||||
# login: "Handle"
|
||||
# will translate User attribute "login" as "Handle"
|
||||
|
|
@ -349,6 +349,27 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# MigrationProxy is used to defer loading of the actual migration classes
|
||||
# until they are needed
|
||||
class MigrationProxy
|
||||
|
||||
attr_accessor :name, :version, :filename
|
||||
|
||||
delegate :migrate, :announce, :write, :to=>:migration
|
||||
|
||||
private
|
||||
|
||||
def migration
|
||||
@migration ||= load_migration
|
||||
end
|
||||
|
||||
def load_migration
|
||||
load(filename)
|
||||
name.constantize
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class Migrator#:nodoc:
|
||||
class << self
|
||||
def migrate(migrations_path, target_version = nil)
|
||||
|
@ -443,17 +464,25 @@ module ActiveRecord
|
|||
runnable.pop if down? && !target.nil?
|
||||
|
||||
runnable.each do |migration|
|
||||
Base.logger.info "Migrating to #{migration} (#{migration.version})"
|
||||
Base.logger.info "Migrating to #{migration.name} (#{migration.version})"
|
||||
|
||||
# On our way up, we skip migrating the ones we've already migrated
|
||||
# On our way down, we skip reverting the ones we've never migrated
|
||||
next if up? && migrated.include?(migration.version.to_i)
|
||||
|
||||
# On our way down, we skip reverting the ones we've never migrated
|
||||
if down? && !migrated.include?(migration.version.to_i)
|
||||
migration.announce 'never migrated, skipping'; migration.write
|
||||
else
|
||||
migration.migrate(@direction)
|
||||
record_version_state_after_migrating(migration.version)
|
||||
next
|
||||
end
|
||||
|
||||
begin
|
||||
ddl_transaction do
|
||||
migration.migrate(@direction)
|
||||
record_version_state_after_migrating(migration.version)
|
||||
end
|
||||
rescue => e
|
||||
canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
|
||||
raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -476,11 +505,10 @@ module ActiveRecord
|
|||
raise DuplicateMigrationNameError.new(name.camelize)
|
||||
end
|
||||
|
||||
load(file)
|
||||
|
||||
klasses << returning(name.camelize.constantize) do |klass|
|
||||
class << klass; attr_accessor :version end
|
||||
klass.version = version
|
||||
klasses << returning(MigrationProxy.new) do |migration|
|
||||
migration.name = name.camelize
|
||||
migration.version = version
|
||||
migration.filename = file
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -519,5 +547,14 @@ module ActiveRecord
|
|||
def down?
|
||||
@direction == :down
|
||||
end
|
||||
|
||||
# Wrap the migration in a transaction only if supported by the adapter.
|
||||
def ddl_transaction(&block)
|
||||
if Base.connection.supports_ddl_transactions?
|
||||
Base.transaction { block.call }
|
||||
else
|
||||
block.call
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
module ActiveRecord
|
||||
module NamedScope
|
||||
# All subclasses of ActiveRecord::Base have two named_scopes:
|
||||
# * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
|
||||
# * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
|
||||
# All subclasses of ActiveRecord::Base have two named \scopes:
|
||||
# * <tt>all</tt> - which is similar to a <tt>find(:all)</tt> query, and
|
||||
# * <tt>scoped</tt> - which allows for the creation of anonymous \scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
|
||||
#
|
||||
# These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
|
||||
# These anonymous \scopes tend to be useful when procedurally generating complex queries, where passing
|
||||
# intermediate values (scopes) around as first-class objects is convenient.
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
|
@ -26,20 +26,20 @@ module ActiveRecord
|
|||
# named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
|
||||
# end
|
||||
#
|
||||
# The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
|
||||
# The above calls to <tt>named_scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
|
||||
# in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
|
||||
#
|
||||
# Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
|
||||
# Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it resembles the association object
|
||||
# constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
|
||||
# <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
|
||||
# as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
|
||||
# <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
|
||||
# as with the association objects, named \scopes act like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
|
||||
# <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really was an Array.
|
||||
#
|
||||
# These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
|
||||
# These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
|
||||
# Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
|
||||
# for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
|
||||
#
|
||||
# All scopes are available as class methods on the ActiveRecord::Base descendent upon which the scopes were defined. But they are also available to
|
||||
# All \scopes are available as class methods on the ActiveRecord::Base descendent upon which the \scopes were defined. But they are also available to
|
||||
# <tt>has_many</tt> associations. If,
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
|
@ -49,7 +49,7 @@ module ActiveRecord
|
|||
# then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
|
||||
# only shirts.
|
||||
#
|
||||
# Named scopes can also be procedural.
|
||||
# Named \scopes can also be procedural:
|
||||
#
|
||||
# class Shirt < ActiveRecord::Base
|
||||
# named_scope :colored, lambda { |color|
|
||||
|
@ -59,7 +59,7 @@ module ActiveRecord
|
|||
#
|
||||
# In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
|
||||
#
|
||||
# Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
|
||||
# Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations:
|
||||
#
|
||||
# class Shirt < ActiveRecord::Base
|
||||
# named_scope :red, :conditions => {:color => 'red'} do
|
||||
|
@ -70,7 +70,7 @@ module ActiveRecord
|
|||
# end
|
||||
#
|
||||
#
|
||||
# For testing complex named scopes, you can examine the scoping options using the
|
||||
# For testing complex named \scopes, you can examine the scoping options using the
|
||||
# <tt>proxy_options</tt> method on the proxy itself.
|
||||
#
|
||||
# class Shirt < ActiveRecord::Base
|
||||
|
@ -101,9 +101,9 @@ module ActiveRecord
|
|||
|
||||
class Scope
|
||||
attr_reader :proxy_scope, :proxy_options
|
||||
|
||||
NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set
|
||||
[].methods.each do |m|
|
||||
unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty\?|respond_to\?)/
|
||||
unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s)
|
||||
delegate m, :to => :proxy_found
|
||||
end
|
||||
end
|
||||
|
@ -136,6 +136,10 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def size
|
||||
@found ? @found.length : count
|
||||
end
|
||||
|
||||
def empty?
|
||||
@found ? @found.empty? : count.zero?
|
||||
end
|
||||
|
@ -144,6 +148,14 @@ module ActiveRecord
|
|||
super || @proxy_scope.respond_to?(method, include_private)
|
||||
end
|
||||
|
||||
def any?
|
||||
if block_given?
|
||||
proxy_found.any? { |*block_args| yield(*block_args) }
|
||||
else
|
||||
!empty?
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def proxy_found
|
||||
@found || load_found
|
||||
|
@ -154,7 +166,8 @@ module ActiveRecord
|
|||
if scopes.include?(method)
|
||||
scopes[method].call(self, *args)
|
||||
else
|
||||
with_scope :find => proxy_options do
|
||||
with_scope :find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {} do
|
||||
method = :new if method == :build
|
||||
proxy_scope.send(method, *args, &block)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,14 +13,15 @@ module ActiveRecord
|
|||
def create_reflection(macro, name, options, active_record)
|
||||
case macro
|
||||
when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
|
||||
reflection = AssociationReflection.new(macro, name, options, active_record)
|
||||
klass = options[:through] ? ThroughReflection : AssociationReflection
|
||||
reflection = klass.new(macro, name, options, active_record)
|
||||
when :composed_of
|
||||
reflection = AggregateReflection.new(macro, name, options, active_record)
|
||||
end
|
||||
write_inheritable_hash :reflections, name => reflection
|
||||
reflection
|
||||
end
|
||||
|
||||
|
||||
# Returns a hash containing all AssociationReflection objects for the current class
|
||||
# Example:
|
||||
#
|
||||
|
@ -30,7 +31,7 @@ module ActiveRecord
|
|||
def reflections
|
||||
read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {})
|
||||
end
|
||||
|
||||
|
||||
# Returns an array of AggregateReflection objects for all the aggregations in the class.
|
||||
def reflect_on_all_aggregations
|
||||
reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) }
|
||||
|
@ -109,7 +110,16 @@ module ActiveRecord
|
|||
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
|
||||
# and +other_aggregation+ has an options hash assigned to it.
|
||||
def ==(other_aggregation)
|
||||
name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
|
||||
other_aggregation.kind_of?(self.class) && name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
|
||||
end
|
||||
|
||||
def sanitized_conditions #:nodoc:
|
||||
@sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
|
||||
end
|
||||
|
||||
# Returns +true+ if +self+ is a +belongs_to+ reflection.
|
||||
def belongs_to?
|
||||
macro == :belongs_to
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -125,10 +135,45 @@ module ActiveRecord
|
|||
|
||||
# Holds all the meta-data about an association as it was specified in the Active Record class.
|
||||
class AssociationReflection < MacroReflection #:nodoc:
|
||||
# Returns the target association's class:
|
||||
#
|
||||
# class Author < ActiveRecord::Base
|
||||
# has_many :books
|
||||
# end
|
||||
#
|
||||
# Author.reflect_on_association(:books).klass
|
||||
# # => Book
|
||||
#
|
||||
# <b>Note:</b> do not call +klass.new+ or +klass.create+ to instantiate
|
||||
# a new association object. Use +build_association+ or +create_association+
|
||||
# instead. This allows plugins to hook into association object creation.
|
||||
def klass
|
||||
@klass ||= active_record.send(:compute_type, class_name)
|
||||
end
|
||||
|
||||
# Returns a new, unsaved instance of the associated class. +options+ will
|
||||
# be passed to the class's constructor.
|
||||
def build_association(*options)
|
||||
klass.new(*options)
|
||||
end
|
||||
|
||||
# Creates a new instance of the associated class, and immediates saves it
|
||||
# with ActiveRecord::Base#save. +options+ will be passed to the class's
|
||||
# creation method. Returns the newly created object.
|
||||
def create_association(*options)
|
||||
klass.create(*options)
|
||||
end
|
||||
|
||||
# Creates a new instance of the associated class, and immediates saves it
|
||||
# with ActiveRecord::Base#save!. +options+ will be passed to the class's
|
||||
# creation method. If the created record doesn't pass validations, then an
|
||||
# exception will be raised.
|
||||
#
|
||||
# Returns the newly created object.
|
||||
def create_association!(*options)
|
||||
klass.create!(*options)
|
||||
end
|
||||
|
||||
def table_name
|
||||
@table_name ||= klass.table_name
|
||||
end
|
||||
|
@ -153,6 +198,52 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def check_validity!
|
||||
end
|
||||
|
||||
def through_reflection
|
||||
false
|
||||
end
|
||||
|
||||
def through_reflection_primary_key_name
|
||||
end
|
||||
|
||||
def source_reflection
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
def derive_class_name
|
||||
class_name = name.to_s.camelize
|
||||
class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
|
||||
class_name
|
||||
end
|
||||
|
||||
def derive_primary_key_name
|
||||
if belongs_to?
|
||||
"#{name}_id"
|
||||
elsif options[:as]
|
||||
"#{options[:as]}_id"
|
||||
else
|
||||
active_record.name.foreign_key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Holds all the meta-data about a :through association as it was specified in the Active Record class.
|
||||
class ThroughReflection < AssociationReflection #:nodoc:
|
||||
# Gets the source of the through reflection. It checks both a singularized and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
|
||||
# (The <tt>:tags</tt> association on Tagging below.)
|
||||
#
|
||||
# class Post < ActiveRecord::Base
|
||||
# has_many :taggings
|
||||
# has_many :tags, :through => :taggings
|
||||
# end
|
||||
#
|
||||
def source_reflection
|
||||
@source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
|
||||
end
|
||||
|
||||
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
|
||||
# of a HasManyThrough or HasOneThrough association. Example:
|
||||
#
|
||||
|
@ -165,7 +256,7 @@ module ActiveRecord
|
|||
# taggings_reflection = tags_reflection.through_reflection
|
||||
#
|
||||
def through_reflection
|
||||
@through_reflection ||= options[:through] ? active_record.reflect_on_association(options[:through]) : false
|
||||
@through_reflection ||= active_record.reflect_on_association(options[:through])
|
||||
end
|
||||
|
||||
# Gets an array of possible <tt>:through</tt> source reflection names:
|
||||
|
@ -176,63 +267,40 @@ module ActiveRecord
|
|||
@source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
|
||||
end
|
||||
|
||||
# Gets the source of the through reflection. It checks both a singularized and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
|
||||
# (The <tt>:tags</tt> association on Tagging below.)
|
||||
#
|
||||
# class Post < ActiveRecord::Base
|
||||
# has_many :taggings
|
||||
# has_many :tags, :through => :taggings
|
||||
# end
|
||||
#
|
||||
def source_reflection
|
||||
return nil unless through_reflection
|
||||
@source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
|
||||
def check_validity!
|
||||
if through_reflection.nil?
|
||||
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
|
||||
end
|
||||
|
||||
if source_reflection.nil?
|
||||
raise HasManyThroughSourceAssociationNotFoundError.new(self)
|
||||
end
|
||||
|
||||
if options[:source_type] && source_reflection.options[:polymorphic].nil?
|
||||
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
|
||||
end
|
||||
|
||||
if source_reflection.options[:polymorphic] && options[:source_type].nil?
|
||||
raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
|
||||
end
|
||||
|
||||
unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil?
|
||||
raise HasManyThroughSourceAssociationMacroError.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
def check_validity!
|
||||
if options[:through]
|
||||
if through_reflection.nil?
|
||||
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
|
||||
end
|
||||
|
||||
if source_reflection.nil?
|
||||
raise HasManyThroughSourceAssociationNotFoundError.new(self)
|
||||
end
|
||||
def through_reflection_primary_key
|
||||
through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name
|
||||
end
|
||||
|
||||
if options[:source_type] && source_reflection.options[:polymorphic].nil?
|
||||
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
|
||||
end
|
||||
|
||||
if source_reflection.options[:polymorphic] && options[:source_type].nil?
|
||||
raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
|
||||
end
|
||||
|
||||
unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil?
|
||||
raise HasManyThroughSourceAssociationMacroError.new(self)
|
||||
end
|
||||
end
|
||||
def through_reflection_primary_key_name
|
||||
through_reflection.primary_key_name if through_reflection.belongs_to?
|
||||
end
|
||||
|
||||
private
|
||||
def derive_class_name
|
||||
# get the class_name of the belongs_to association of the through reflection
|
||||
if through_reflection
|
||||
options[:source_type] || source_reflection.class_name
|
||||
else
|
||||
class_name = name.to_s.camelize
|
||||
class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
|
||||
class_name
|
||||
end
|
||||
end
|
||||
|
||||
def derive_primary_key_name
|
||||
if macro == :belongs_to
|
||||
"#{name}_id"
|
||||
elsif options[:as]
|
||||
"#{options[:as]}_id"
|
||||
else
|
||||
active_record.name.foreign_key
|
||||
end
|
||||
options[:source_type] || source_reflection.class_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -102,7 +102,7 @@ HEADER
|
|||
spec[:precision] = column.precision.inspect if !column.precision.nil?
|
||||
spec[:scale] = column.scale.inspect if !column.scale.nil?
|
||||
spec[:null] = 'false' if !column.null
|
||||
spec[:default] = default_string(column.default) if !column.default.nil?
|
||||
spec[:default] = default_string(column.default) if column.has_default?
|
||||
(spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
|
||||
spec
|
||||
end.compact
|
||||
|
|
|
@ -37,11 +37,26 @@ module ActiveRecord
|
|||
$queries_executed = []
|
||||
yield
|
||||
ensure
|
||||
assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed."
|
||||
assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}"
|
||||
end
|
||||
|
||||
def assert_no_queries(&block)
|
||||
assert_queries(0, &block)
|
||||
end
|
||||
|
||||
def self.use_concurrent_connections
|
||||
setup :connection_allow_concurrency_setup
|
||||
teardown :connection_allow_concurrency_teardown
|
||||
end
|
||||
|
||||
def connection_allow_concurrency_setup
|
||||
@connection = ActiveRecord::Base.remove_connection
|
||||
ActiveRecord::Base.establish_connection(@connection.merge({:allow_concurrency => true}))
|
||||
end
|
||||
|
||||
def connection_allow_concurrency_teardown
|
||||
ActiveRecord::Base.clear_all_connections!
|
||||
ActiveRecord::Base.establish_connection(@connection)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
require 'thread'
|
||||
|
||||
module ActiveRecord
|
||||
module Transactions # :nodoc:
|
||||
# See ActiveRecord::Transactions::ClassMethods for documentation.
|
||||
module Transactions
|
||||
class TransactionError < ActiveRecordError # :nodoc:
|
||||
end
|
||||
|
||||
|
@ -15,26 +16,33 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action.
|
||||
# The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succeeded and
|
||||
# vice versa. Transactions enforce the integrity of the database and guard the data against program errors or database break-downs.
|
||||
# So basically you should use transaction blocks whenever you have a number of statements that must be executed together or
|
||||
# not at all. Example:
|
||||
# Transactions are protective blocks where SQL statements are only permanent
|
||||
# if they can all succeed as one atomic action. The classic example is a
|
||||
# transfer between two accounts where you can only have a deposit if the
|
||||
# withdrawal succeeded and vice versa. Transactions enforce the integrity of
|
||||
# the database and guard the data against program errors or database
|
||||
# break-downs. So basically you should use transaction blocks whenever you
|
||||
# have a number of statements that must be executed together or not at all.
|
||||
# Example:
|
||||
#
|
||||
# transaction do
|
||||
# ActiveRecord::Base.transaction do
|
||||
# david.withdrawal(100)
|
||||
# mary.deposit(100)
|
||||
# end
|
||||
#
|
||||
# This example will only take money from David and give to Mary if neither +withdrawal+ nor +deposit+ raises an exception.
|
||||
# Exceptions will force a ROLLBACK that returns the database to the state before the transaction was begun. Be aware, though,
|
||||
# that the objects will _not_ have their instance data returned to their pre-transactional state.
|
||||
# This example will only take money from David and give to Mary if neither
|
||||
# +withdrawal+ nor +deposit+ raises an exception. Exceptions will force a
|
||||
# ROLLBACK that returns the database to the state before the transaction was
|
||||
# begun. Be aware, though, that the objects will _not_ have their instance
|
||||
# data returned to their pre-transactional state.
|
||||
#
|
||||
# == Different Active Record classes in a single transaction
|
||||
#
|
||||
# Though the transaction class method is called on some Active Record class,
|
||||
# the objects within the transaction block need not all be instances of
|
||||
# that class.
|
||||
# that class. This is because transactions are per-database connection, not
|
||||
# per-model.
|
||||
#
|
||||
# In this example a <tt>Balance</tt> record is transactionally saved even
|
||||
# though <tt>transaction</tt> is called on the <tt>Account</tt> class:
|
||||
#
|
||||
|
@ -43,6 +51,14 @@ module ActiveRecord
|
|||
# account.save!
|
||||
# end
|
||||
#
|
||||
# Note that the +transaction+ method is also available as a model instance
|
||||
# method. For example, you can also do this:
|
||||
#
|
||||
# balance.transaction do
|
||||
# balance.save!
|
||||
# account.save!
|
||||
# end
|
||||
#
|
||||
# == Transactions are not distributed across database connections
|
||||
#
|
||||
# A transaction acts on a single database connection. If you have
|
||||
|
@ -62,48 +78,72 @@ module ActiveRecord
|
|||
#
|
||||
# == Save and destroy are automatically wrapped in a transaction
|
||||
#
|
||||
# Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks
|
||||
# will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction
|
||||
# depends on or you can raise exceptions in the callbacks to rollback.
|
||||
# Both Base#save and Base#destroy come wrapped in a transaction that ensures
|
||||
# that whatever you do in validations or callbacks will happen under the
|
||||
# protected cover of a transaction. So you can use validations to check for
|
||||
# values that the transaction depends on or you can raise exceptions in the
|
||||
# callbacks to rollback, including <tt>after_*</tt> callbacks.
|
||||
#
|
||||
# == Exception handling
|
||||
# == Exception handling and rolling back
|
||||
#
|
||||
# Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you
|
||||
# should be ready to catch those in your application code. One exception is the ActiveRecord::Rollback exception, which will
|
||||
# trigger a ROLLBACK when raised, but not be re-raised by the transaction block.
|
||||
# Also have in mind that exceptions thrown within a transaction block will
|
||||
# be propagated (after triggering the ROLLBACK), so you should be ready to
|
||||
# catch those in your application code.
|
||||
#
|
||||
# One exception is the ActiveRecord::Rollback exception, which will trigger
|
||||
# a ROLLBACK when raised, but not be re-raised by the transaction block.
|
||||
#
|
||||
# *Warning*: one should not catch ActiveRecord::StatementInvalid exceptions
|
||||
# inside a transaction block. StatementInvalid exceptions indicate that an
|
||||
# error occurred at the database level, for example when a unique constraint
|
||||
# is violated. On some database systems, such as PostgreSQL, database errors
|
||||
# inside a transaction causes the entire transaction to become unusable
|
||||
# until it's restarted from the beginning. Here is an example which
|
||||
# demonstrates the problem:
|
||||
#
|
||||
# # Suppose that we have a Number model with a unique column called 'i'.
|
||||
# Number.transaction do
|
||||
# Number.create(:i => 0)
|
||||
# begin
|
||||
# # This will raise a unique constraint error...
|
||||
# Number.create(:i => 0)
|
||||
# rescue ActiveRecord::StatementInvalid
|
||||
# # ...which we ignore.
|
||||
# end
|
||||
#
|
||||
# # On PostgreSQL, the transaction is now unusable. The following
|
||||
# # statement will cause a PostgreSQL error, even though the unique
|
||||
# # constraint is no longer violated:
|
||||
# Number.create(:i => 1)
|
||||
# # => "PGError: ERROR: current transaction is aborted, commands
|
||||
# # ignored until end of transaction block"
|
||||
# end
|
||||
#
|
||||
# One should restart the entire transaction if a StatementError occurred.
|
||||
module ClassMethods
|
||||
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
|
||||
def transaction(&block)
|
||||
increment_open_transactions
|
||||
connection.increment_open_transactions
|
||||
|
||||
begin
|
||||
connection.transaction(Thread.current['start_db_transaction'], &block)
|
||||
connection.transaction(connection.open_transactions == 1, &block)
|
||||
ensure
|
||||
decrement_open_transactions
|
||||
connection.decrement_open_transactions
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def increment_open_transactions #:nodoc:
|
||||
open = Thread.current['open_transactions'] ||= 0
|
||||
Thread.current['start_db_transaction'] = open.zero?
|
||||
Thread.current['open_transactions'] = open + 1
|
||||
end
|
||||
|
||||
def decrement_open_transactions #:nodoc:
|
||||
Thread.current['open_transactions'] -= 1
|
||||
end
|
||||
end
|
||||
|
||||
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
|
||||
def transaction(&block)
|
||||
self.class.transaction(&block)
|
||||
end
|
||||
|
||||
def destroy_with_transactions #:nodoc:
|
||||
transaction { destroy_without_transactions }
|
||||
with_transaction_returning_status(:destroy_without_transactions)
|
||||
end
|
||||
|
||||
def save_with_transactions(perform_validation = true) #:nodoc:
|
||||
rollback_active_record_state! { transaction { save_without_transactions(perform_validation) } }
|
||||
rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, perform_validation) }
|
||||
end
|
||||
|
||||
def save_with_transactions! #:nodoc:
|
||||
|
@ -126,5 +166,20 @@ module ActiveRecord
|
|||
end
|
||||
raise
|
||||
end
|
||||
|
||||
# Executes +method+ within a transaction and captures its return value as a
|
||||
# status flag. If the status is true the transaction is committed, otherwise
|
||||
# a ROLLBACK is issued. In any case the status flag is returned.
|
||||
#
|
||||
# This method is available within the context of an ActiveRecord::Base
|
||||
# instance.
|
||||
def with_transaction_returning_status(method, *args)
|
||||
status = nil
|
||||
transaction do
|
||||
status = send(method, *args)
|
||||
raise ActiveRecord::Rollback unless status
|
||||
end
|
||||
status
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
module ActiveRecord
|
||||
# Raised by save! and create! when the record is invalid. Use the
|
||||
# Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the
|
||||
# +record+ method to retrieve the record which did not validate.
|
||||
# begin
|
||||
# complex_operation_that_calls_save!_internally
|
||||
|
@ -18,70 +18,97 @@ module ActiveRecord
|
|||
# determine whether the object is in a valid state to be saved. See usage example in Validations.
|
||||
class Errors
|
||||
include Enumerable
|
||||
|
||||
class << self
|
||||
def default_error_messages
|
||||
ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use I18n.translate('activerecord.errors.messages').")
|
||||
I18n.translate 'activerecord.errors.messages'
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(base) # :nodoc:
|
||||
@base, @errors = base, {}
|
||||
end
|
||||
|
||||
@@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",
|
||||
:greater_than => "must be greater than %d",
|
||||
:greater_than_or_equal_to => "must be greater than or equal to %d",
|
||||
:equal_to => "must be equal to %d",
|
||||
:less_than => "must be less than %d",
|
||||
:less_than_or_equal_to => "must be less than or equal to %d",
|
||||
:odd => "must be odd",
|
||||
:even => "must be even"
|
||||
}
|
||||
|
||||
# Holds a hash with all the default error messages that can be replaced by your own copy or localizations.
|
||||
cattr_accessor :default_error_messages
|
||||
|
||||
|
||||
# Adds an error to the base object instead of any particular attribute. This is used
|
||||
# to report errors that don't tie to any specific attribute, but rather to the object
|
||||
# as a whole. These error messages don't get prepended with any field name when iterating
|
||||
# with each_full, so they should be complete sentences.
|
||||
# with +each_full+, so they should be complete sentences.
|
||||
def add_to_base(msg)
|
||||
add(:base, msg)
|
||||
end
|
||||
|
||||
# Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
|
||||
# Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
|
||||
# for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one
|
||||
# error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
|
||||
# If no +msg+ is supplied, "invalid" is assumed.
|
||||
def add(attribute, msg = @@default_error_messages[:invalid])
|
||||
@errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
|
||||
@errors[attribute.to_s] << msg
|
||||
# If no +messsage+ is supplied, :invalid is assumed.
|
||||
# If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
|
||||
def add(attribute, message = nil, options = {})
|
||||
message ||= :invalid
|
||||
message = generate_message(attribute, message, options) if message.is_a?(Symbol)
|
||||
@errors[attribute.to_s] ||= []
|
||||
@errors[attribute.to_s] << message
|
||||
end
|
||||
|
||||
# Will add an error message to each of the attributes in +attributes+ that is empty.
|
||||
def add_on_empty(attributes, msg = @@default_error_messages[:empty])
|
||||
def add_on_empty(attributes, custom_message = nil)
|
||||
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
|
||||
is_empty = value.respond_to?(:empty?) ? value.empty? : false
|
||||
add(attr, :empty, :default => custom_message) unless !value.nil? && !is_empty
|
||||
end
|
||||
end
|
||||
|
||||
# Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
|
||||
def add_on_blank(attributes, msg = @@default_error_messages[:blank])
|
||||
def add_on_blank(attributes, custom_message = nil)
|
||||
for attr in [attributes].flatten
|
||||
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
|
||||
add(attr, msg) if value.blank?
|
||||
add(attr, :blank, :default => custom_message) if value.blank?
|
||||
end
|
||||
end
|
||||
|
||||
# Translates an error message in it's default scope (<tt>activerecord.errrors.messages</tt>).
|
||||
# Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there,
|
||||
# it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the
|
||||
# default message (e.g. <tt>activerecord.errors.messages.MESSAGE</tt>). The translated model name,
|
||||
# translated attribute name and the value are available for interpolation.
|
||||
#
|
||||
# When using inheritence in your models, it will check all the inherited models too, but only if the model itself
|
||||
# hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt>
|
||||
# error +message+ for the <tt>title</tt> +attribute+, it looks for these translations:
|
||||
#
|
||||
# <ol>
|
||||
# <li><tt>activerecord.errors.models.admin.attributes.title.blank</tt></li>
|
||||
# <li><tt>activerecord.errors.models.admin.blank</tt></li>
|
||||
# <li><tt>activerecord.errors.models.user.attributes.title.blank</tt></li>
|
||||
# <li><tt>activerecord.errors.models.user.blank</tt></li>
|
||||
# <li><tt>activerecord.errors.messages.blank</tt></li>
|
||||
# <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li>
|
||||
# </ol>
|
||||
def generate_message(attribute, message = :invalid, options = {})
|
||||
|
||||
message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)
|
||||
|
||||
defaults = @base.class.self_and_descendents_from_active_record.map do |klass|
|
||||
[ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}",
|
||||
:"models.#{klass.name.underscore}.#{message}" ]
|
||||
end
|
||||
|
||||
defaults << options.delete(:default)
|
||||
defaults = defaults.compact.flatten << :"messages.#{message}"
|
||||
|
||||
key = defaults.shift
|
||||
value = @base.respond_to?(attribute) ? @base.send(attribute) : nil
|
||||
|
||||
options = { :default => defaults,
|
||||
:model => @base.class.human_name,
|
||||
:attribute => @base.class.human_attribute_name(attribute.to_s),
|
||||
:value => value,
|
||||
:scope => [:activerecord, :errors]
|
||||
}.merge(options)
|
||||
|
||||
I18n.translate(key, options)
|
||||
end
|
||||
|
||||
# Returns true if the specified +attribute+ has errors associated with it.
|
||||
#
|
||||
|
@ -97,7 +124,7 @@ module ActiveRecord
|
|||
!@errors[attribute.to_s].nil?
|
||||
end
|
||||
|
||||
# Returns nil, if no errors are associated with the specified +attribute+.
|
||||
# Returns +nil+, if no errors are associated with the specified +attribute+.
|
||||
# Returns the error message, if one error is associated with the specified +attribute+.
|
||||
# Returns an array of error messages, if more than one error is associated with the specified +attribute+.
|
||||
#
|
||||
|
@ -118,7 +145,7 @@ module ActiveRecord
|
|||
|
||||
alias :[] :on
|
||||
|
||||
# Returns errors assigned to the base object through add_to_base according to the normal rules of on(attribute).
|
||||
# Returns errors assigned to the base object through +add_to_base+ according to the normal rules of <tt>on(attribute)</tt>.
|
||||
def on_base
|
||||
on(:base)
|
||||
end
|
||||
|
@ -131,15 +158,15 @@ module ActiveRecord
|
|||
# end
|
||||
#
|
||||
# company = Company.create(:address => '123 First St.')
|
||||
# company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } # =>
|
||||
# name - is too short (minimum is 5 characters)
|
||||
# name - can't be blank
|
||||
# address - can't be blank
|
||||
# company.errors.each{|attr,msg| puts "#{attr} - #{msg}" }
|
||||
# # => name - is too short (minimum is 5 characters)
|
||||
# # name - can't be blank
|
||||
# # address - can't be blank
|
||||
def each
|
||||
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
|
||||
end
|
||||
|
||||
# Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
|
||||
# Yields each full error message added. So <tt>Person.errors.add("first_name", "can't be empty")</tt> will be returned
|
||||
# through iteration as "First name can't be empty".
|
||||
#
|
||||
# class Company < ActiveRecord::Base
|
||||
|
@ -148,10 +175,10 @@ module ActiveRecord
|
|||
# end
|
||||
#
|
||||
# company = Company.create(:address => '123 First St.')
|
||||
# company.errors.each_full{|msg| puts msg } # =>
|
||||
# Name is too short (minimum is 5 characters)
|
||||
# Name can't be blank
|
||||
# Address can't be blank
|
||||
# company.errors.each_full{|msg| puts msg }
|
||||
# # => Name is too short (minimum is 5 characters)
|
||||
# # Name can't be blank
|
||||
# # Address can't be blank
|
||||
def each_full
|
||||
full_messages.each { |msg| yield msg }
|
||||
end
|
||||
|
@ -166,22 +193,24 @@ module ActiveRecord
|
|||
# company = Company.create(:address => '123 First St.')
|
||||
# company.errors.full_messages # =>
|
||||
# ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]
|
||||
def full_messages
|
||||
def full_messages(options = {})
|
||||
full_messages = []
|
||||
|
||||
@errors.each_key do |attr|
|
||||
@errors[attr].each do |msg|
|
||||
next if msg.nil?
|
||||
@errors[attr].each do |message|
|
||||
next unless message
|
||||
|
||||
if attr == "base"
|
||||
full_messages << msg
|
||||
full_messages << message
|
||||
else
|
||||
full_messages << @base.class.human_attribute_name(attr) + " " + msg
|
||||
#key = :"activerecord.att.#{@base.class.name.underscore.to_sym}.#{attr}"
|
||||
attr_name = @base.class.human_attribute_name(attr)
|
||||
full_messages << attr_name + ' ' + message
|
||||
end
|
||||
end
|
||||
end
|
||||
full_messages
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if no errors have been added.
|
||||
def empty?
|
||||
|
@ -209,13 +238,13 @@ module ActiveRecord
|
|||
# end
|
||||
#
|
||||
# company = Company.create(:address => '123 First St.')
|
||||
# company.errors.to_xml # =>
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <errors>
|
||||
# <error>Name is too short (minimum is 5 characters)</error>
|
||||
# <error>Name can't be blank</error>
|
||||
# <error>Address can't be blank</error>
|
||||
# </errors>
|
||||
# company.errors.to_xml
|
||||
# # => <?xml version="1.0" encoding="UTF-8"?>
|
||||
# # <errors>
|
||||
# # <error>Name is too short (minimum is 5 characters)</error>
|
||||
# # <error>Name can't be blank</error>
|
||||
# # <error>Address can't be blank</error>
|
||||
# # </errors>
|
||||
def to_xml(options={})
|
||||
options[:root] ||= "errors"
|
||||
options[:indent] ||= 2
|
||||
|
@ -226,9 +255,12 @@ module ActiveRecord
|
|||
full_messages.each { |msg| e.error(msg) }
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
# Please do have a look at ActiveRecord::Validations::ClassMethods for a higher level of validations.
|
||||
#
|
||||
# Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and
|
||||
# +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring
|
||||
# that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
|
||||
|
@ -261,14 +293,12 @@ module ActiveRecord
|
|||
# person.errors.on "phone_number" # => "has invalid format"
|
||||
# person.errors.each_full { |msg| puts msg }
|
||||
# # => "Last name can't be empty\n" +
|
||||
# "Phone number has invalid format"
|
||||
# # "Phone number has invalid format"
|
||||
#
|
||||
# person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
|
||||
# person.save # => true (and person is now saved in the database)
|
||||
#
|
||||
# An Errors object is automatically created for every Active Record.
|
||||
#
|
||||
# Please do have a look at ActiveRecord::Validations::ClassMethods for a higher level of validations.
|
||||
module Validations
|
||||
VALIDATIONS = %w( validate validate_on_create validate_on_update )
|
||||
|
||||
|
@ -277,16 +307,56 @@ module ActiveRecord
|
|||
base.class_eval do
|
||||
alias_method_chain :save, :validation
|
||||
alias_method_chain :save!, :validation
|
||||
alias_method_chain :update_attribute, :validation_skipping
|
||||
end
|
||||
|
||||
base.send :include, ActiveSupport::Callbacks
|
||||
base.define_callbacks *VALIDATIONS
|
||||
end
|
||||
|
||||
# All of the following validations are defined in the class scope of the model that you're interested in validating.
|
||||
# They offer a more declarative way of specifying when the model is valid and when it is not. It is recommended to use
|
||||
# these over the low-level calls to +validate+ and +validate_on_create+ when possible.
|
||||
# Active Record classes can implement validations in several ways. The highest level, easiest to read,
|
||||
# and recommended approach is to use the declarative <tt>validates_..._of</tt> class methods (and
|
||||
# +validates_associated+) documented below. These are sufficient for most model validations.
|
||||
#
|
||||
# Slightly lower level is +validates_each+. It provides some of the same options as the purely declarative
|
||||
# validation methods, but like all the lower-level approaches it requires manually adding to the errors collection
|
||||
# when the record is invalid.
|
||||
#
|
||||
# At a yet lower level, a model can use the class methods +validate+, +validate_on_create+ and +validate_on_update+
|
||||
# to add validation methods or blocks. These are ActiveSupport::Callbacks and follow the same rules of inheritance
|
||||
# and chaining.
|
||||
#
|
||||
# The lowest level style is to define the instance methods +validate+, +validate_on_create+ and +validate_on_update+
|
||||
# as documented in ActiveRecord::Validations.
|
||||
#
|
||||
# == +validate+, +validate_on_create+ and +validate_on_update+ Class Methods
|
||||
#
|
||||
# Calls to these methods add a validation method or block to the class. Again, this approach is recommended
|
||||
# only when the higher-level methods documented below (<tt>validates_..._of</tt> and +validates_associated+) are
|
||||
# insufficient to handle the required validation.
|
||||
#
|
||||
# This can be done with a symbol pointing to a method:
|
||||
#
|
||||
# class Comment < ActiveRecord::Base
|
||||
# validate :must_be_friends
|
||||
#
|
||||
# def must_be_friends
|
||||
# errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Or with a block which is passed the current record to be validated:
|
||||
#
|
||||
# class Comment < ActiveRecord::Base
|
||||
# validate do |comment|
|
||||
# comment.must_be_friends
|
||||
# end
|
||||
#
|
||||
# def must_be_friends
|
||||
# errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# This usage applies to +validate_on_create+ and +validate_on_update+ as well.
|
||||
module ClassMethods
|
||||
DEFAULT_VALIDATION_OPTIONS = {
|
||||
:on => :save,
|
||||
|
@ -300,34 +370,6 @@ module ActiveRecord
|
|||
:equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=',
|
||||
:odd => 'odd?', :even => 'even?' }.freeze
|
||||
|
||||
# Adds a validation method or block to the class. This is useful when
|
||||
# overriding the +validate+ instance method becomes too unwieldly and
|
||||
# you're looking for more descriptive declaration of your validations.
|
||||
#
|
||||
# This can be done with a symbol pointing to a method:
|
||||
#
|
||||
# class Comment < ActiveRecord::Base
|
||||
# validate :must_be_friends
|
||||
#
|
||||
# def must_be_friends
|
||||
# errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Or with a block which is passed the current record to be validated:
|
||||
#
|
||||
# class Comment < ActiveRecord::Base
|
||||
# validate do |comment|
|
||||
# comment.must_be_friends
|
||||
# end
|
||||
#
|
||||
# def must_be_friends
|
||||
# errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# This usage applies to +validate_on_create+ and +validate_on_update+ as well.
|
||||
|
||||
# Validates each attribute against a block.
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
|
@ -389,13 +431,15 @@ module ActiveRecord
|
|||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
def validates_confirmation_of(*attr_names)
|
||||
configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save }
|
||||
configuration = { :on => :save }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
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")
|
||||
unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
|
||||
record.errors.add(attr_name, :confirmation, :default => configuration[:message])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -423,19 +467,21 @@ module ActiveRecord
|
|||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
def validates_acceptance_of(*attr_names)
|
||||
configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" }
|
||||
configuration = { :on => :save, :allow_nil => true, :accept => "1" }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
db_cols = begin
|
||||
column_names
|
||||
rescue ActiveRecord::StatementInvalid
|
||||
rescue Exception # To ignore both statement and connection errors
|
||||
[]
|
||||
end
|
||||
names = attr_names.reject { |name| db_cols.include?(name.to_s) }
|
||||
attr_accessor(*names)
|
||||
|
||||
validates_each(attr_names,configuration) do |record, attr_name, value|
|
||||
record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept]
|
||||
unless value == configuration[:accept]
|
||||
record.errors.add(attr_name, :accepted, :default => configuration[:message])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -462,7 +508,7 @@ module ActiveRecord
|
|||
# method, proc or string should return or evaluate to a true or false value.
|
||||
#
|
||||
def validates_presence_of(*attr_names)
|
||||
configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save }
|
||||
configuration = { :on => :save }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
# can't use validates_each here, because it cannot cope with nonexistent attributes,
|
||||
|
@ -476,13 +522,13 @@ module ActiveRecord
|
|||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# validates_length_of :first_name, :maximum=>30
|
||||
# validates_length_of :last_name, :maximum=>30, :message=>"less than %d if you don't mind"
|
||||
# validates_length_of :last_name, :maximum=>30, :message=>"less than {{count}} if you don't mind"
|
||||
# validates_length_of :fax, :in => 7..32, :allow_nil => true
|
||||
# validates_length_of :phone, :in => 7..32, :allow_blank => true
|
||||
# validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name"
|
||||
# validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least %d character"
|
||||
# validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with %d characters... don't play me."
|
||||
# validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least %d words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
|
||||
# validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least {{count}} character"
|
||||
# validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with {{count}} characters... don't play me."
|
||||
# validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least {{count}} words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
|
||||
# end
|
||||
#
|
||||
# Configuration options:
|
||||
|
@ -493,9 +539,9 @@ module ActiveRecord
|
|||
# * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>.
|
||||
# * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
|
||||
# * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
|
||||
# * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)").
|
||||
# * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)").
|
||||
# * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)").
|
||||
# * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is {{count}} characters)").
|
||||
# * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is {{count}} characters)").
|
||||
# * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be {{count}} characters)").
|
||||
# * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message.
|
||||
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
|
||||
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
|
@ -510,10 +556,7 @@ module ActiveRecord
|
|||
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],
|
||||
:tokenizer => lambda {|value| value.split(//)}
|
||||
:tokenizer => lambda {|value| value.split(//)}
|
||||
}.merge(DEFAULT_VALIDATION_OPTIONS)
|
||||
options.update(attrs.extract_options!.symbolize_keys)
|
||||
|
||||
|
@ -536,15 +579,12 @@ module ActiveRecord
|
|||
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|
|
||||
value = options[:tokenizer].call(value) if value.kind_of?(String)
|
||||
if value.nil? or value.size < option_value.begin
|
||||
record.errors.add(attr, too_short)
|
||||
record.errors.add(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
|
||||
elsif value.size > option_value.end
|
||||
record.errors.add(attr, too_long)
|
||||
record.errors.add(attr, :too_long, :default => options[:too_long], :count => option_value.end)
|
||||
end
|
||||
end
|
||||
when :is, :minimum, :maximum
|
||||
|
@ -554,11 +594,13 @@ module ActiveRecord
|
|||
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|
|
||||
value = options[:tokenizer].call(value) if value.kind_of?(String)
|
||||
record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
|
||||
unless !value.nil? and value.size.method(validity_checks[option])[option_value]
|
||||
key = message_options[option]
|
||||
custom_message = options[:message] || options[key]
|
||||
record.errors.add(attr, key, :default => custom_message, :count => option_value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -583,14 +625,10 @@ module ActiveRecord
|
|||
# When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified
|
||||
# attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself.
|
||||
#
|
||||
# Because this check is performed outside the database there is still a chance that duplicate values
|
||||
# will be inserted in two parallel transactions. To guarantee against this you should create a
|
||||
# unique index on the field. See +add_index+ for more information.
|
||||
#
|
||||
# Configuration options:
|
||||
# * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken").
|
||||
# * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint.
|
||||
# * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+false+ by default).
|
||||
# * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default).
|
||||
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
|
||||
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
|
||||
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
|
@ -599,8 +637,72 @@ module ActiveRecord
|
|||
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
#
|
||||
# === Concurrency and integrity
|
||||
#
|
||||
# Using this validation method in conjunction with ActiveRecord::Base#save
|
||||
# does not guarantee the absence of duplicate record insertions, because
|
||||
# uniqueness checks on the application level are inherently prone to race
|
||||
# conditions. For example, suppose that two users try to post a Comment at
|
||||
# the same time, and a Comment's title must be unique. At the database-level,
|
||||
# the actions performed by these users could be interleaved in the following manner:
|
||||
#
|
||||
# User 1 | User 2
|
||||
# ------------------------------------+--------------------------------------
|
||||
# # User 1 checks whether there's |
|
||||
# # already a comment with the title |
|
||||
# # 'My Post'. This is not the case. |
|
||||
# SELECT * FROM comments |
|
||||
# WHERE title = 'My Post' |
|
||||
# |
|
||||
# | # User 2 does the same thing and also
|
||||
# | # infers that his title is unique.
|
||||
# | SELECT * FROM comments
|
||||
# | WHERE title = 'My Post'
|
||||
# |
|
||||
# # User 1 inserts his comment. |
|
||||
# INSERT INTO comments |
|
||||
# (title, content) VALUES |
|
||||
# ('My Post', 'hi!') |
|
||||
# |
|
||||
# | # User 2 does the same thing.
|
||||
# | INSERT INTO comments
|
||||
# | (title, content) VALUES
|
||||
# | ('My Post', 'hello!')
|
||||
# |
|
||||
# | # ^^^^^^
|
||||
# | # Boom! We now have a duplicate
|
||||
# | # title!
|
||||
#
|
||||
# This could even happen if you use transactions with the 'serializable'
|
||||
# isolation level. There are several ways to get around this problem:
|
||||
# - By locking the database table before validating, and unlocking it after
|
||||
# saving. However, table locking is very expensive, and thus not
|
||||
# recommended.
|
||||
# - By locking a lock file before validating, and unlocking it after saving.
|
||||
# This does not work if you've scaled your Rails application across
|
||||
# multiple web servers (because they cannot share lock files, or cannot
|
||||
# do that efficiently), and thus not recommended.
|
||||
# - Creating a unique index on the field, by using
|
||||
# ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the
|
||||
# rare case that a race condition occurs, the database will guarantee
|
||||
# the field's uniqueness.
|
||||
#
|
||||
# When the database catches such a duplicate insertion,
|
||||
# ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid
|
||||
# exception. You can either choose to let this error propagate (which
|
||||
# will result in the default Rails exception page being shown), or you
|
||||
# can catch it and restart the transaction (e.g. by telling the user
|
||||
# that the title already exists, and asking him to re-enter the title).
|
||||
# This technique is also known as optimistic concurrency control:
|
||||
# http://en.wikipedia.org/wiki/Optimistic_concurrency_control
|
||||
#
|
||||
# Active Record currently provides no way to distinguish unique
|
||||
# index constraint errors from other types of database errors, so you
|
||||
# will have to parse the (database-specific) exception message to detect
|
||||
# such a case.
|
||||
def validates_uniqueness_of(*attr_names)
|
||||
configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true }
|
||||
configuration = { :case_sensitive => true }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
validates_each(attr_names,configuration) do |record, attr_name, value|
|
||||
|
@ -620,18 +722,23 @@ module ActiveRecord
|
|||
|
||||
is_text_column = finder_class.columns_hash[attr_name.to_s].text?
|
||||
|
||||
if !value.nil? && is_text_column
|
||||
if value.nil?
|
||||
comparison_operator = "IS ?"
|
||||
elsif is_text_column
|
||||
comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
|
||||
value = value.to_s
|
||||
else
|
||||
comparison_operator = "= ?"
|
||||
end
|
||||
|
||||
sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
|
||||
|
||||
if value.nil? || (configuration[:case_sensitive] || !is_text_column)
|
||||
condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}"
|
||||
condition_sql = "#{sql_attribute} #{comparison_operator}"
|
||||
condition_params = [value]
|
||||
else
|
||||
# sqlite has case sensitive SELECT query, while MySQL/Postgresql don't.
|
||||
# Hence, this is needed only for sqlite.
|
||||
condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}"
|
||||
condition_params = [value.chars.downcase]
|
||||
condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}"
|
||||
condition_params = [value.mb_chars.downcase]
|
||||
end
|
||||
|
||||
if scope = configuration[:scope]
|
||||
|
@ -647,26 +754,10 @@ module ActiveRecord
|
|||
condition_params << record.send(:id)
|
||||
end
|
||||
|
||||
results = finder_class.with_exclusive_scope do
|
||||
connection.select_all(
|
||||
construct_finder_sql(
|
||||
:select => "#{connection.quote_column_name(attr_name)}",
|
||||
:from => "#{finder_class.quoted_table_name}",
|
||||
:conditions => [condition_sql, *condition_params]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
unless results.length.zero?
|
||||
found = true
|
||||
|
||||
# As MySQL/Postgres don't have case sensitive SELECT queries, we try to find duplicate
|
||||
# column in ruby when case sensitive option
|
||||
if configuration[:case_sensitive] && finder_class.columns_hash[attr_name.to_s].text?
|
||||
found = results.any? { |a| a[attr_name.to_s] == value }
|
||||
finder_class.with_exclusive_scope do
|
||||
if finder_class.exists?([condition_sql, *condition_params])
|
||||
record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
|
||||
end
|
||||
|
||||
record.errors.add(attr_name, configuration[:message]) if found
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -696,13 +787,15 @@ module ActiveRecord
|
|||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
def validates_format_of(*attr_names)
|
||||
configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
|
||||
configuration = { :on => :save, :with => nil }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
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] % value) unless value.to_s =~ configuration[:with]
|
||||
unless value.to_s =~ configuration[:with]
|
||||
record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -711,7 +804,7 @@ module ActiveRecord
|
|||
# class Person < ActiveRecord::Base
|
||||
# validates_inclusion_of :gender, :in => %w( m f ), :message => "woah! what are you then!??!!"
|
||||
# validates_inclusion_of :age, :in => 0..99
|
||||
# validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %s is not included in the list"
|
||||
# validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension {{value}} is not included in the list"
|
||||
# end
|
||||
#
|
||||
# Configuration options:
|
||||
|
@ -726,15 +819,17 @@ module ActiveRecord
|
|||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
def validates_inclusion_of(*attr_names)
|
||||
configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save }
|
||||
configuration = { :on => :save }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
enum = configuration[:in] || configuration[:within]
|
||||
|
||||
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
|
||||
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?)
|
||||
|
||||
validates_each(attr_names, configuration) do |record, attr_name, value|
|
||||
record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value)
|
||||
unless enum.include?(value)
|
||||
record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -743,7 +838,7 @@ module ActiveRecord
|
|||
# class Person < ActiveRecord::Base
|
||||
# validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here"
|
||||
# validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60"
|
||||
# validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %s is not allowed"
|
||||
# validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension {{value}} is not allowed"
|
||||
# end
|
||||
#
|
||||
# Configuration options:
|
||||
|
@ -758,15 +853,17 @@ module ActiveRecord
|
|||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
def validates_exclusion_of(*attr_names)
|
||||
configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save }
|
||||
configuration = { :on => :save }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
enum = configuration[:in] || configuration[:within]
|
||||
|
||||
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
|
||||
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?)
|
||||
|
||||
validates_each(attr_names, configuration) do |record, attr_name, value|
|
||||
record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value)
|
||||
if enum.include?(value)
|
||||
record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -802,12 +899,13 @@ module ActiveRecord
|
|||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
def validates_associated(*attr_names)
|
||||
configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save }
|
||||
configuration = { :on => :save }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
validates_each(attr_names, configuration) do |record, attr_name, value|
|
||||
record.errors.add(attr_name, configuration[:message]) unless
|
||||
(value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
|
||||
unless (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
|
||||
record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -855,15 +953,15 @@ module ActiveRecord
|
|||
|
||||
if configuration[:only_integer]
|
||||
unless raw_value.to_s =~ /\A[+-]?\d+\Z/
|
||||
record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
|
||||
record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
|
||||
next
|
||||
end
|
||||
raw_value = raw_value.to_i
|
||||
else
|
||||
begin
|
||||
begin
|
||||
raw_value = Kernel.Float(raw_value)
|
||||
rescue ArgumentError, TypeError
|
||||
record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
|
||||
record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
|
||||
next
|
||||
end
|
||||
end
|
||||
|
@ -871,11 +969,11 @@ module ActiveRecord
|
|||
numericality_options.each do |option|
|
||||
case option
|
||||
when :odd, :even
|
||||
record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
|
||||
unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
|
||||
record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message])
|
||||
end
|
||||
else
|
||||
message = configuration[:message] || ActiveRecord::Errors.default_error_messages[option]
|
||||
message = message % configuration[option] if configuration[option]
|
||||
record.errors.add(attr_name, message) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
|
||||
record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -924,14 +1022,6 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Updates a single attribute and saves the record without going through the normal validation procedure.
|
||||
# This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
|
||||
# in Base is replaced with this when the validations module is mixed in, which it is by default.
|
||||
def update_attribute_with_validation_skipping(name, value)
|
||||
send(name.to_s + '=', value)
|
||||
save(false)
|
||||
end
|
||||
|
||||
# Runs +validate+ and +validate_on_create+ or +validate_on_update+ and returns true if no errors were added otherwise false.
|
||||
def valid?
|
||||
errors.clear
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
module ActiveRecord
|
||||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 1
|
||||
TINY = 1
|
||||
MINOR = 2
|
||||
TINY = 0
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue