Permalink
Browse files

Add scoping and unscoped as the syntax to replace the old with_scope …

…and with_exclusive_scope. A few examples:

* with_scope now should be scoping:

Before:

  Comment.with_scope(:find => { :conditions => { :post_id => 1 } }) do
    Comment.first #=> SELECT * FROM comments WHERE post_id = 1
  end

After:

  Comment.where(:post_id => 1).scoping do
    Comment.first #=> SELECT * FROM comments WHERE post_id = 1
  end

* with_exclusive_scope now should be unscoped:

  class Post < ActiveRecord::Base
    default_scope :published => true
  end

  Post.all #=> SELECT * FROM posts WHERE published = true

Before:

  Post.with_exclusive_scope do
    Post.all #=> SELECT * FROM posts
  end

After:

  Post.unscoped do
    Post.all #=> SELECT * FROM posts
  end

Notice you can also use unscoped without a block and it will return an anonymous scope with default_scope values:

  Post.unscoped.all #=> SELECT * FROM posts
  • Loading branch information...
1 parent 9013227 commit bd1666ad1de88598ed6f04ceffb8488a77be4385 @josevalim josevalim committed Jun 29, 2010
@@ -398,7 +398,7 @@ def colorize_logging(*args)
delegate :find, :first, :last, :all, :destroy, :destroy_all, :exists?, :delete, :delete_all, :update, :update_all, :to => :scoped
delegate :find_each, :find_in_batches, :to => :scoped
- delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped
+ delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped
delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped
# Executes a custom SQL query against your database and returns all the results. The results will
@@ -801,7 +801,7 @@ def column_methods_hash #:nodoc:
def reset_column_information
undefine_attribute_methods
@column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil
- @arel_engine = @unscoped = @arel_table = nil
+ @arel_engine = @relation = @arel_table = nil
end
def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc:
@@ -904,9 +904,9 @@ def sti_name
store_full_sti_class ? name : name.demodulize
end
- def unscoped
- @unscoped ||= Relation.new(self, arel_table)
- finder_needs_type_condition? ? @unscoped.where(type_condition) : @unscoped
+ def relation
+ @relation ||= Relation.new(self, arel_table)
+ finder_needs_type_condition? ? @relation.where(type_condition) : @relation
end
def arel_table
@@ -923,6 +923,31 @@ def arel_engine
end
end
+ # Returns a scope for this class without taking into account the default_scope.
+ #
+ # class Post < ActiveRecord::Base
+ # default_scope :published => true
+ # end
+ #
+ # Post.all # Fires "SELECT * FROM posts WHERE published = true"
+ # Post.unscoped.all # Fires "SELECT * FROM posts"
+ #
+ # This method also accepts a block meaning that all queries inside the block will
+ # not use the default_scope:
+ #
+ # Post.unscoped {
+ # limit(10) # Fires "SELECT * FROM posts LIMIT 10"
+ # }
+ #
+ def unscoped
+ block_given? ? relation.scoping { yield } : relation
+ end
+
+ def scoped_methods #:nodoc:
+ key = :"#{self}_scoped_methods"
+ Thread.current[key] = Thread.current[key].presence || self.default_scoping.dup
+ end
+
private
# Finder methods must instantiate through this method to work with the
# single-table inheritance model that makes it possible to create
@@ -1183,11 +1208,6 @@ def default_scope(options = {})
self.default_scoping << construct_finder_arel(options, default_scoping.pop)
end
- def scoped_methods #:nodoc:
- key = :"#{self}_scoped_methods"
- Thread.current[key] = Thread.current[key].presence || self.default_scoping.dup
- end
-
def current_scoped_methods #:nodoc:
scoped_methods.last
end
@@ -25,10 +25,9 @@ module ClassMethods
#
# You can define a \scope that applies to all finders using
# ActiveRecord::Base.default_scope.
- def scoped(options = {}, &block)
+ def scoped(options = nil)
if options.present?
- relation = scoped.apply_finder_options(options)
- block_given? ? relation.extending(Module.new(&block)) : relation
+ scoped.apply_finder_options(options)
else
current_scoped_methods ? unscoped.merge(current_scoped_methods) : unscoped.clone
end
@@ -88,18 +87,22 @@ def scopes
# end
def scope(name, scope_options = {}, &block)
name = name.to_sym
+ valid_scope_name?(name)
- if !scopes[name] && respond_to?(name, true)
- logger.warn "Creating scope :#{name}. " \
- "Overwriting existing method #{self.name}.#{name}."
- end
+ extension = Module.new(&block) if block_given?
scopes[name] = lambda do |*args|
options = scope_options.is_a?(Proc) ? scope_options.call(*args) : scope_options
- relation = scoped
- relation = options.is_a?(Hash) ? relation.apply_finder_options(options) : scoped.merge(options) if options
- block_given? ? relation.extending(Module.new(&block)) : relation
+ relation = if options.is_a?(Hash)
+ scoped.apply_finder_options(options)
+ elsif options
+ scoped.merge(options)
+ else
+ scoped
+ end
+
+ extension ? relation.extending(extension) : relation
end
singleton_class.send :define_method, name, &scopes[name]
@@ -109,7 +112,15 @@ def named_scope(*args, &block)
ActiveSupport::Deprecation.warn("Base.named_scope has been deprecated, please use Base.scope instead", caller)
scope(*args, &block)
end
- end
+ protected
+
+ def valid_scope_name?(name)
+ if !scopes[name] && respond_to?(name, true)
+ logger.warn "Creating scope :#{name}. " \
+ "Overwriting existing method #{self.name}.#{name}."
+ end
+ end
+ end
end
end
@@ -182,7 +182,7 @@ def toggle!(attribute)
def reload(options = nil)
clear_aggregation_cache
clear_association_cache
- @attributes.update(self.class.send(:with_exclusive_scope) { self.class.find(self.id, options) }.instance_variable_get('@attributes'))
+ @attributes.update(self.class.unscoped { self.class.find(self.id, options) }.instance_variable_get('@attributes'))
@attributes_cache = {}
self
end
@@ -16,7 +16,7 @@ class Relation
attr_reader :table, :klass
attr_accessor :extensions
- def initialize(klass, table, &block)
+ def initialize(klass, table)
@klass, @table = klass, table
@implicit_readonly = nil
@@ -25,12 +25,10 @@ def initialize(klass, table, &block)
SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)}
(ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])}
@extensions = []
-
- apply_modules(Module.new(&block)) if block_given?
end
def new(*args, &block)
- with_create_scope { @klass.new(*args, &block) }
+ scoping { @klass.new(*args, &block) }
end
def initialize_copy(other)
@@ -40,11 +38,11 @@ def initialize_copy(other)
alias build new
def create(*args, &block)
- with_create_scope { @klass.create(*args, &block) }
+ scoping { @klass.create(*args, &block) }
end
def create!(*args, &block)
- with_create_scope { @klass.create!(*args, &block) }
+ scoping { @klass.create!(*args, &block) }
end
def respond_to?(method, include_private = false)
@@ -102,6 +100,25 @@ def many?
end
end
+ # Scope all queries to the current scope.
+ #
+ # ==== Example
+ #
+ # Comment.where(:post_id => 1).scoping do
+ # Comment.first #=> SELECT * FROM comments WHERE post_id = 1
+ # end
+ #
+ # Please check unscoped if you want to remove all previous scopes (including
+ # the default_scope) during the execution of a block.
+ def scoping
+ @klass.scoped_methods << self
+ begin
+ yield
+ ensure
+ @klass.scoped_methods.pop
+ end
+ end
+
# Updates all records with details given if they match a set of conditions supplied, limits and order can
# also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the
# database. It does not instantiate the involved models and it does not trigger Active Record callbacks
@@ -305,7 +322,6 @@ def scope_for_create
if where.is_a?(Arel::Predicates::Equality)
hash[where.operand1.name] = where.operand2.respond_to?(:value) ? where.operand2.value : where.operand2
end
-
hash
end
end
@@ -328,15 +344,6 @@ def inspect
to_a.inspect
end
- def extend(*args, &block)
- if block_given?
- apply_modules Module.new(&block)
- self
- else
- super
- end
- end
-
protected
def method_missing(method, *args, &block)
@@ -364,10 +371,6 @@ def method_missing(method, *args, &block)
private
- def with_create_scope
- @klass.send(:with_scope, :create => scope_for_create, :find => {}) { yield }
- end
-
def references_eager_loaded_tables?
# always convert table names to downcase as in Oracle quoted table names are in uppercase
joined_tables = (tables_in_string(arel.joins(arel)) + [table.name, table.table_alias]).compact.map(&:downcase).uniq
@@ -86,8 +86,9 @@ def from(value = true)
clone.tap { |r| r.from_value = value }
end
- def extending(*modules)
- clone.tap { |r| r.send :apply_modules, *modules }
+ def extending(*modules, &block)
+ modules << Module.new(&block) if block_given?
+ clone.tap { |r| r.send(:apply_modules, *modules) }
end
def reverse_order
@@ -173,7 +173,7 @@ def test_alt_find_first_within_inheritance
def test_complex_inheritance
very_special_client = VerySpecialClient.create("name" => "veryspecial")
- assert_equal very_special_client, VerySpecialClient.find(:first, :conditions => "name = 'veryspecial'")
+ assert_equal very_special_client, VerySpecialClient.where("name = 'veryspecial'").first
assert_equal very_special_client, SpecialClient.find(:first, :conditions => "name = 'veryspecial'")
assert_equal very_special_client, Company.find(:first, :conditions => "name = 'veryspecial'")
assert_equal very_special_client, Client.find(:first, :conditions => "name = 'veryspecial'")
Oops, something went wrong.

2 comments on commit bd1666a

Contributor

nragaz replied Jun 29, 2010

Bravo to this style of commit message for those of us trying to build apps off of HEAD!

Horray!

Post.unscoped.all

is much cleaner than:

Post.send(:with_exclusive_scope) do
  Post.all
end

Nice job with this! Lets hope it doesn't get reverted down the track.

Please sign in to comment.