diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 625a98a8f5..5eb64e75f1 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* with_scope is protected. #8524 [Josh Peek] + * Quickref for association methods. #7723 [marclove, Mindsweeper] * Calculations: return nil average instead of 0 when there are no rows to average. #8298 [davidw] diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index bda72637c3..4da3f15abe 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -85,14 +85,14 @@ module ActiveRecord end def create(attrs = {}) - record = @reflection.klass.with_scope(:create => construct_scope[:create]) { @reflection.klass.create(attrs) } + record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.create(attrs) } @target ||= [] unless loaded? @target << record record end def create!(attrs = {}) - record = @reflection.klass.with_scope(:create => construct_scope[:create]) { @reflection.klass.create!(attrs) } + record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.create!(attrs) } @target ||= [] unless loaded? @target << record record @@ -161,7 +161,7 @@ module ActiveRecord if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) super else - @reflection.klass.with_scope(construct_scope) { @reflection.klass.send(method, *args, &block) } + @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.send(method, *args, &block) } end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 93f1b2ee6a..7a55334912 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -57,7 +57,7 @@ module ActiveRecord raise_on_type_mismatch(associate) raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, through) unless associate.respond_to?(:new_record?) && !associate.new_record? - @owner.send(@reflection.through_reflection.name).proxy_target << klass.with_scope(:create => construct_join_attributes(associate)) { klass.create! } + @owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(associate)) { klass.create! } @target << associate if loaded? end end @@ -91,7 +91,7 @@ module ActiveRecord def create!(attrs = nil) @reflection.klass.transaction do - self << @reflection.klass.with_scope(:create => attrs) { @reflection.klass.create! } + self << @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create! } end end @@ -105,7 +105,7 @@ module ActiveRecord if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) super else - @reflection.klass.with_scope(construct_scope) { @reflection.klass.send(method, *args, &block) } + @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.send(method, *args, &block) } end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index b18169ab67..59e5486ad6 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -80,7 +80,7 @@ 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.with_scope(:create => construct_scope[:create]) { yield @reflection.klass } + record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { yield @reflection.klass } if replace_existing replace(record, true) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 931afa61ba..54f3a8252d 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -947,91 +947,6 @@ module ActiveRecord #:nodoc: logger.level = old_logger_level if logger end - # Scope parameters to method calls within the block. Takes a hash of method_name => parameters hash. - # method_name may be :find or :create. :find parameters may include the :conditions, :joins, - # :include, :offset, :limit, and :readonly options. :create parameters are an attributes hash. - # - # Article.with_scope(:find => { :conditions => "blog_id = 1" }, :create => { :blog_id => 1 }) do - # Article.find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1 - # a = Article.create(1) - # a.blog_id # => 1 - # end - # - # In nested scopings, all previous parameters are overwritten by inner rule - # except :conditions in :find, that are merged as hash. - # - # Article.with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }, :create => { :blog_id => 1 }) do - # Article.with_scope(:find => { :limit => 10}) - # Article.find(:all) # => SELECT * from articles WHERE blog_id = 1 LIMIT 10 - # end - # Article.with_scope(:find => { :conditions => "author_id = 3" }) - # Article.find(:all) # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1 - # end - # end - # - # You can ignore any previous scopings by using with_exclusive_scope method. - # - # Article.with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }) do - # Article.with_exclusive_scope(:find => { :limit => 10 }) - # Article.find(:all) # => SELECT * from articles LIMIT 10 - # end - # end - def with_scope(method_scoping = {}, action = :merge, &block) - method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping) - - # Dup first and second level of hash (method and params). - method_scoping = method_scoping.inject({}) do |hash, (method, params)| - hash[method] = (params == true) ? params : params.dup - hash - end - - method_scoping.assert_valid_keys([ :find, :create ]) - - if f = method_scoping[:find] - f.assert_valid_keys([ :conditions, :joins, :select, :include, :from, :offset, :limit, :order, :readonly, :lock ]) - set_readonly_option! f - end - - # Merge scopings - if action == :merge && current_scoped_methods - method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)| - case hash[method] - when Hash - if method == :find - (hash[method].keys + params.keys).uniq.each do |key| - merge = hash[method][key] && params[key] # merge if both scopes have the same key - if key == :conditions && merge - hash[method][key] = [params[key], hash[method][key]].collect{ |sql| "( %s )" % sanitize_sql(sql) }.join(" AND ") - elsif key == :include && merge - hash[method][key] = merge_includes(hash[method][key], params[key]).uniq - else - hash[method][key] = hash[method][key] || params[key] - end - end - else - hash[method] = params.merge(hash[method]) - end - else - hash[method] = params - end - hash - end - end - - self.scoped_methods << method_scoping - - begin - yield - ensure - self.scoped_methods.pop - end - end - - # Works like with_scope, but discards any nested properties. - def with_exclusive_scope(method_scoping = {}, &block) - with_scope(method_scoping, :overwrite, &block) - end - # Overwrite the default class equality method to provide support for association proxies. def ===(object) object.is_a?(self) @@ -1409,6 +1324,103 @@ module ActiveRecord #:nodoc: end protected + # Scope parameters to method calls within the block. Takes a hash of method_name => parameters hash. + # method_name may be :find or :create. :find parameters may include the :conditions, :joins, + # :include, :offset, :limit, and :readonly options. :create parameters are an attributes hash. + # + # class Article < ActiveRecord::Base + # def self.create_with_scope + # with_scope(:find => { :conditions => "blog_id = 1" }, :create => { :blog_id => 1 }) do + # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1 + # a = create(1) + # a.blog_id # => 1 + # end + # end + # end + # + # In nested scopings, all previous parameters are overwritten by inner rule + # except :conditions in :find, that are merged as hash. + # + # class Article < ActiveRecord::Base + # def self.find_with_scope + # with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }, :create => { :blog_id => 1 }) do + # with_scope(:find => { :limit => 10}) + # find(:all) # => SELECT * from articles WHERE blog_id = 1 LIMIT 10 + # end + # with_scope(:find => { :conditions => "author_id = 3" }) + # find(:all) # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1 + # end + # end + # end + # end + # + # You can ignore any previous scopings by using with_exclusive_scope method. + # + # class Article < ActiveRecord::Base + # def self.find_with_exclusive_scope + # with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }) do + # with_exclusive_scope(:find => { :limit => 10 }) + # find(:all) # => SELECT * from articles LIMIT 10 + # end + # end + # end + # end + def with_scope(method_scoping = {}, action = :merge, &block) + method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping) + + # Dup first and second level of hash (method and params). + method_scoping = method_scoping.inject({}) do |hash, (method, params)| + hash[method] = (params == true) ? params : params.dup + hash + end + + method_scoping.assert_valid_keys([ :find, :create ]) + + if f = method_scoping[:find] + f.assert_valid_keys([ :conditions, :joins, :select, :include, :from, :offset, :limit, :order, :readonly, :lock ]) + set_readonly_option! f + end + + # Merge scopings + if action == :merge && current_scoped_methods + method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)| + case hash[method] + when Hash + if method == :find + (hash[method].keys + params.keys).uniq.each do |key| + merge = hash[method][key] && params[key] # merge if both scopes have the same key + if key == :conditions && merge + hash[method][key] = [params[key], hash[method][key]].collect{ |sql| "( %s )" % sanitize_sql(sql) }.join(" AND ") + elsif key == :include && merge + hash[method][key] = merge_includes(hash[method][key], params[key]).uniq + else + hash[method][key] = hash[method][key] || params[key] + end + end + else + hash[method] = params.merge(hash[method]) + end + else + hash[method] = params + end + hash + end + end + + self.scoped_methods << method_scoping + + begin + yield + ensure + self.scoped_methods.pop + end + end + + # Works like with_scope, but discards any nested properties. + def with_exclusive_scope(method_scoping = {}, &block) + with_scope(method_scoping, :overwrite, &block) + end + def subclasses #:nodoc: @@subclasses[self] ||= [] @@subclasses[self] + extra = @@subclasses[self].inject([]) {|list, subclass| list + subclass.subclasses } diff --git a/activerecord/test/abstract_unit.rb b/activerecord/test/abstract_unit.rb index c3d4dbe591..1317f8dae3 100755 --- a/activerecord/test/abstract_unit.rb +++ b/activerecord/test/abstract_unit.rb @@ -75,5 +75,10 @@ ActiveRecord::Base.connection.class.class_eval do end end +# Make with_scope public for tests +class << ActiveRecord::Base + public :with_scope, :with_exclusive_scope +end + #ActiveRecord::Base.logger = Logger.new(STDOUT) #ActiveRecord::Base.colorize_logging = false