Skip to content

Commit

Permalink
Refactor and cleanup in some ActiveRecord modules
Browse files Browse the repository at this point in the history
* Avoid double hash lookups in AR::Reflection when reflecting associations/aggregations
* Minor cleanups: use elsif, do..end, if..else instead of unless..else
* Simplify DynamicMatchers#respond_to?
* Use "where" instead of scoped with conditions hash
* Extract `scoped_by` method pattern regexp to constant
* Extract noisy class_eval from method_missing in dynamic matchers
* Extract readonly check, avoid calling column#to_s twice in persistence
* Refactor predicate builder, remove some variables
  • Loading branch information
carlosantoniodasilva committed Mar 3, 2012
1 parent eafa58b commit 50725ce
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 68 deletions.
30 changes: 14 additions & 16 deletions activerecord/lib/active_record/autosave_association.rb
Expand Up @@ -191,23 +191,21 @@ def add_autosave_association_callbacks(reflection)
# Doesn't use after_save as that would save associations added in after_create/after_update twice
after_create save_method
after_update save_method
elsif reflection.macro == :has_one
define_method(save_method) { save_has_one_association(reflection) }
# Configures two callbacks instead of a single after_save so that
# the model may rely on their execution order relative to its
# own callbacks.
#
# For example, given that after_creates run before after_saves, if
# we configured instead an after_save there would be no way to fire
# a custom after_create callback after the child association gets
# created.
after_create save_method
after_update save_method
else
if reflection.macro == :has_one
define_method(save_method) { save_has_one_association(reflection) }
# Configures two callbacks instead of a single after_save so that
# the model may rely on their execution order relative to its
# own callbacks.
#
# For example, given that after_creates run before after_saves, if
# we configured instead an after_save there would be no way to fire
# a custom after_create callback after the child association gets
# created.
after_create save_method
after_update save_method
else
define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) }
before_save save_method
end
define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) }
before_save save_method
end
end

Expand Down
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/counter_cache.rb
Expand Up @@ -71,7 +71,7 @@ def update_counters(id, counters)

IdentityMap.remove_by_id(symbolized_base_class, id) if IdentityMap.enabled?

update_all(updates.join(', '), primary_key => id )
update_all(updates.join(', '), primary_key => id)
end

# Increment a number field by one, usually representing a count.
Expand Down
42 changes: 23 additions & 19 deletions activerecord/lib/active_record/dynamic_matchers.rb
@@ -1,13 +1,10 @@
module ActiveRecord
module DynamicMatchers
def respond_to?(method_id, include_private = false)
if match = DynamicFinderMatch.match(method_id)
return true if all_attributes_exists?(match.attribute_names)
elsif match = DynamicScopeMatch.match(method_id)
return true if all_attributes_exists?(match.attribute_names)
end
match = find_dynamic_match(method_id)
valid_match = match && all_attributes_exists?(match.attribute_names)

super
valid_match || super
end

private
Expand All @@ -22,22 +19,18 @@ def respond_to?(method_id, include_private = false)
# Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it
# is first invoked, so that future attempts to use it do not run through method_missing.
def method_missing(method_id, *arguments, &block)
if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id))
if match = find_dynamic_match(method_id)
attribute_names = match.attribute_names
super unless all_attributes_exists?(attribute_names)

unless match.valid_arguments?(arguments)
method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'"
backtrace = [method_trace] + caller
raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace
end

if match.respond_to?(:scope?) && match.scope?
self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)]
#
scoped(:conditions => attributes) # scoped(:conditions => attributes)
end # end
METHOD
define_scope_method(method_id, attribute_names)
send(method_id, *arguments)
elsif match.finder?
options = arguments.extract_options!
Expand All @@ -51,17 +44,30 @@ def self.#{method_id}(*args) # def self.scope
end
end

def define_scope_method(method_id, attribute_names) #:nodoc
self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
conditions = Hash[[:#{attribute_names.join(',:')}].zip(args)] # conditions = Hash[[:user_name, :password].zip(args)]
where(conditions) # where(conditions)
end # end
METHOD
end

def find_dynamic_match(method_id) #:nodoc:
DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id)
end

# Similar in purpose to +expand_hash_conditions_for_aggregates+.
def expand_attribute_names_for_aggregates(attribute_names)
attribute_names.map { |attribute_name|
unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
attribute_names.map do |attribute_name|
if aggregation = reflect_on_aggregation(attribute_name.to_sym)
aggregate_mapping(aggregation).map do |field_attr, _|
field_attr.to_sym
end
else
attribute_name.to_sym
end
}.flatten
end.flatten
end

def all_attributes_exists?(attribute_names)
Expand All @@ -73,7 +79,5 @@ def aggregate_mapping(reflection)
mapping = reflection.options[:mapping] || [reflection.name, reflection.name]
mapping.first.is_a?(Array) ? mapping : [mapping]
end


end
end
7 changes: 5 additions & 2 deletions activerecord/lib/active_record/dynamic_scope_match.rb
Expand Up @@ -7,9 +7,12 @@ module ActiveRecord
# chain more <tt>scoped_by_* </tt> methods after the other. It acts like a named
# scope except that it's dynamic.
class DynamicScopeMatch
METHOD_PATTERN = /^scoped_by_([_a-zA-Z]\w*)$/

def self.match(method)
return unless method.to_s =~ /^scoped_by_([_a-zA-Z]\w*)$/
new(true, $1 && $1.split('_and_'))
if method.to_s =~ METHOD_PATTERN
new(true, $1 && $1.split('_and_'))
end
end

def initialize(scope, attribute_names)
Expand Down
11 changes: 8 additions & 3 deletions activerecord/lib/active_record/persistence.rb
Expand Up @@ -176,7 +176,7 @@ def becomes(klass)
#
def update_attribute(name, value)
name = name.to_s
raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
verify_readonly_attribute(name)
send("#{name}=", value)
save(:validate => false)
end
Expand All @@ -191,7 +191,7 @@ def update_attribute(name, value)
# attribute is marked as readonly.
def update_column(name, value)
name = name.to_s
raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
verify_readonly_attribute(name)
raise ActiveRecordError, "can not update on a new record object" unless persisted?
raw_write_attribute(name, value)
self.class.update_all({ name => value }, self.class.primary_key => id) == 1
Expand Down Expand Up @@ -323,7 +323,8 @@ def touch(name = nil)
changes = {}

attributes.each do |column|
changes[column.to_s] = write_attribute(column.to_s, current_time)
column = column.to_s
changes[column] = write_attribute(column, current_time)
end

changes[self.class.locking_column] = increment_lock if locking_enabled?
Expand Down Expand Up @@ -369,5 +370,9 @@ def create
@new_record = false
id
end

def verify_readonly_attribute(name)
raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
end
end
end
18 changes: 9 additions & 9 deletions activerecord/lib/active_record/reflection.rb
Expand Up @@ -23,11 +23,11 @@ module Reflection # :nodoc:
module ClassMethods
def create_reflection(macro, name, options, active_record)
case macro
when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
klass = options[:through] ? ThroughReflection : AssociationReflection
reflection = klass.new(macro, name, options, active_record)
when :composed_of
reflection = AggregateReflection.new(macro, name, options, active_record)
when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
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

self.reflections = self.reflections.merge(name => reflection)
Expand All @@ -44,7 +44,8 @@ def reflect_on_all_aggregations
# Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
#
def reflect_on_aggregation(aggregation)
reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil
reflection = reflections[aggregation]
reflection if reflection.is_a?(AggregateReflection)
end

# Returns an array of AssociationReflection objects for all the
Expand All @@ -68,7 +69,8 @@ def reflect_on_all_associations(macro = nil)
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
def reflect_on_association(association)
reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
reflection = reflections[association]
reflection if reflection.is_a?(AssociationReflection)
end

# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
Expand All @@ -77,7 +79,6 @@ def reflect_on_all_autosave_associations
end
end


# Abstract base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
class MacroReflection
Expand Down Expand Up @@ -452,7 +453,6 @@ def conditions
# Recursively fill out the rest of the array from the through reflection
conditions += through_conditions

# And return
conditions
end
end
Expand Down
27 changes: 13 additions & 14 deletions activerecord/lib/active_record/relation/predicate_builder.rb
@@ -1,7 +1,7 @@
module ActiveRecord
class PredicateBuilder # :nodoc:
def self.build_from_hash(engine, attributes, default_table)
predicates = attributes.map do |column, value|
attributes.map do |column, value|
table = default_table

if value.is_a?(Hash)
Expand All @@ -17,20 +17,18 @@ def self.build_from_hash(engine, attributes, default_table)

build(table[column.to_sym], value)
end
end
predicates.flatten
end.flatten
end

def self.references(attributes)
references = attributes.map do |key, value|
attributes.map do |key, value|
if value.is_a?(Hash)
key
else
key = key.to_s
key.split('.').first.to_sym if key.include?('.')
end
end
references.compact
end.compact
end

private
Expand All @@ -43,23 +41,24 @@ def self.build(attribute, value)
values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? x.id : x}
ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)}

array_predicates = ranges.map {|range| attribute.in(range)}

if values.include?(nil)
values_predicate = if values.include?(nil)
values = values.compact

case values.length
when 0
array_predicates << attribute.eq(nil)
attribute.eq(nil)
when 1
array_predicates << attribute.eq(values.first).or(attribute.eq(nil))
attribute.eq(values.first).or(attribute.eq(nil))
else
array_predicates << attribute.in(values).or(attribute.eq(nil))
attribute.in(values).or(attribute.eq(nil))
end
else
array_predicates << attribute.in(values)
attribute.in(values)
end

array_predicates.inject {|composite, predicate| composite.or(predicate)}
array_predicates = ranges.map { |range| attribute.in(range) }
array_predicates << values_predicate
array_predicates.inject { |composite, predicate| composite.or(predicate) }
when Range, Arel::Relation
attribute.in(value)
when ActiveRecord::Model
Expand Down
8 changes: 4 additions & 4 deletions activerecord/lib/active_record/sanitization.rb
Expand Up @@ -37,9 +37,9 @@ def sanitize_sql_for_conditions(condition, table_name = self.table_name)
# { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'"
def sanitize_sql_for_assignment(assignments)
case assignments
when Array; sanitize_sql_array(assignments)
when Hash; sanitize_sql_hash_for_assignment(assignments)
else assignments
when Array; sanitize_sql_array(assignments)
when Hash; sanitize_sql_hash_for_assignment(assignments)
else assignments
end
end

Expand All @@ -57,7 +57,7 @@ def sanitize_sql_for_assignment(assignments)
def expand_hash_conditions_for_aggregates(attrs)
expanded_attrs = {}
attrs.each do |attr, value|
unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil?
if aggregation = reflect_on_aggregation(attr.to_sym)
mapping = aggregate_mapping(aggregation)
mapping.each do |field_attr, aggregate_attr|
if mapping.size == 1 && !value.respond_to?(aggregate_attr)
Expand Down

0 comments on commit 50725ce

Please sign in to comment.