* Copyright (C) 2006 Dimitrij Denissenko
* http://www.dvisionfactory.com
* http://retro.dvisionfactory.com/global-scope
* svn://dvisionfactory.com/rails/plugins/global_scope
* Please read LICENCE document for more information.
This plugin allows to define ActiveRecord's method parameters with a global
scope. The functionality is analogous to the original with_scope method,
but the impact is exactly the opposite: with_scope applies selective
scopes within given blocks, global_scope however acts globally but
allows exceptional behaviour within witout_global_scope blocks. Example:
# with_scope impact example
class Article < ActiveRecord::Base
end
Article.find(:all) # -> SELECT * FROM articles
with_scope(:find => {:conditions => ["author_id = ?", 1]}) do
Article.find(:all) # -> SELECT * FROM articles WHERE author_id = 1
end
# global_scope impact example
class Article < ActiveRecord::Base
global_scope(:find_committed_to_author, :find => {:conditions => ["author_id = ?", 1]})
end
Article.find(:all) # -> SELECT * FROM articles WHERE author_id = 1
without_global_scope(:find_committed_to_author) do
Article.find(:all) # -> SELECT * FROM articles
end
== Background
The plugin was originally not meant to be used in a usual application development
process because general scopes are almost never a good idea. The purpose of the
provided code is to fix an side-effect issue of multiple aliasing of
ActiveRecord's 'find' method. Details of the issue and a possible integration
example are given in the next chapters.
=== Side-effects of "find aliasing" - an example
Think of an example application that uses the
acts_as_paranoid[http://www.agilewebdevelopment.com/plugins/acts_as_paranoid]
and the default_order[http://www.agilewebdevelopment.com/plugins/default_order]
plugins. Both alias the 'find' method, to append conditions and enhance its
functionality:
default_order
alias_method :find_without_order, :find
alias_method :find, :find_with_order
def find_with_order(*args)
... # new find method
end
acts_as_paranoid
alias_method :find_with_deleted, :find
...
def find(*args)
... # new find method
end
A problem occurs as soon as a model uses two(or more) 'find' manipulating
extensions at the same time. Example:
class Author < ActiveRecord::Base
order_by "last_name"
acts_as_paranoid
...
end
The first 'order_by' call aliases the original 'find' to 'find_without_order'
and defines a 'find_with_order', which basically is the new 'find'. The second
alias ('acts_as_paranoid') makes 'find_with_order' to 'find_with_deleted' and
introduces a new, own 'find'.
So what exactly happens? The original, unaliased 'find' method has the
following behaviour:
Author.find(:all) # SELECT * FROM authors
After 'order_by "last_name"' the behaviour changes to:
Author.find_without_order(:all) # SELECT * FROM authors
Author.find(:all) # SELECT * FROM authors ORDER BY last_name
And finally, after 'acts_as_paranoid':
Author.find_with_deleted(:all) # SELECT * FROM authors ORDER BY last_name
Author.find(:all) # SELECT * FROM authors WHERE deleted_at IS NULL ORDER BY last_name
Some might already see sources for possible defects. Doing multiple 'find'
aliasing almost certainly results in side-effects. Although in the example
above, 'find' should (per definition) return only not-deleted records (those
where 'deleted_at' is nil), a call like
Author.find_without_order(:all, :order => "pre_name")
would break this definition and perform
SELECT * FROM authors ORDER BY pre_name
statement, which certainly does not reflect the intention of the programmer.
=== Integration with existing applications
Just a short example of "How existing applications could be integrated with
my global_scope plugin?":
Again, I will take recently published default_order extension as an example.
The original code looks like that:
module DefaultOrder
...
module ClassMethods
def order_by(order_string)
self.class_eval %{
class << self
def find_with_order(*args)
if args[1]
args[1][:order] = "#{order_string}" if args[1].is_a?(Hash) && !args[1][:order]
else
args[1] = {:order => "#{order_string}"}
end
find_without_order(*args)
end
alias_method :find_without_order, :find
alias_method :find, :find_with_order
end
}
end
end
end
A possible integration of global_scope could be:
module DefaultOrder
...
module ClassMethods
def order_by(order_string)
self.class_eval do
if self.respond_to?(:global_scope) # new way ... one single code line
global_scope(:set_by_default_order_plugin, :find => {:order => order_string})
else # old way
class << self
def find_with_order(*args)
if args[1]
args[1][:order] = "#{order_string}" if args[1].is_a?(Hash) && !args[1][:order]
else
args[1] = {:order => "#{order_string}"}
end
find_without_order(*args)
end
alias_method :find_without_order, :find
alias_method :find, :find_with_order
end
end
end
end
end
end