Skip to content

Commit

Permalink
Add scoping and unscoped as the syntax to replace the old with_scope …
Browse files Browse the repository at this point in the history
…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
josevalim committed Jun 29, 2010
1 parent 9013227 commit bd1666a
Show file tree
Hide file tree
Showing 10 changed files with 484 additions and 250 deletions.
40 changes: 30 additions & 10 deletions activerecord/lib/active_record/base.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
33 changes: 22 additions & 11 deletions activerecord/lib/active_record/named_scope.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand All @@ -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
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/persistence.rb
Expand Up @@ -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
Expand Down
43 changes: 23 additions & 20 deletions activerecord/lib/active_record/relation.rb
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions activerecord/lib/active_record/relation/query_methods.rb
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/cases/inheritance_test.rb
Expand Up @@ -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'")
Expand Down

2 comments on commit bd1666a

@nragaz
Copy link
Contributor

@nragaz nragaz commented on bd1666a Jun 29, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@KieranP
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.