1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
require 'active_support/concern'
module ActiveRecord
module Scoping
extend ActiveSupport::Concern
included do
include Default
include Named
end
module ClassMethods
# with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be
# <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while
# <tt>:create</tt> parameters are an attributes hash.
#
# class Article < ActiveRecord::Base
# def self.create_with_scope
# with_scope(:find => where(: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 the innermost rule, with the exception of
# <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged.
#
# <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing
# problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the
# array of strings format for your joins.
#
# class Article < ActiveRecord::Base
# def self.find_with_scope
# with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do
# with_scope(:find => limit(10)) do
# all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10
# end
# with_scope(:find => where(:author_id => 3)) do
# 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 the <tt>with_exclusive_scope</tt> method.
#
# class Article < ActiveRecord::Base
# def self.find_with_exclusive_scope
# with_scope(:find => where(:blog_id => 1).limit(1)) do
# with_exclusive_scope(:find => limit(10)) do
# all # => SELECT * from articles LIMIT 10
# end
# end
# end
# end
#
# *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+.
def with_scope(scope = {}, action = :merge, &block)
# If another Active Record class has been passed in, get its current scope
scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope)
previous_scope = self.current_scope
if scope.is_a?(Hash)
# Dup first and second level of hash (method and params).
scope = scope.dup
scope.each do |method, params|
scope[method] = params.dup unless params == true
end
scope.assert_valid_keys([ :find, :create ])
relation = construct_finder_arel(scope[:find] || {})
relation.default_scoped = true unless action == :overwrite
if previous_scope && previous_scope.create_with_value && scope[:create]
scope_for_create = if action == :merge
previous_scope.create_with_value.merge(scope[:create])
else
scope[:create]
end
relation = relation.create_with(scope_for_create)
else
scope_for_create = scope[:create]
scope_for_create ||= previous_scope.create_with_value if previous_scope
relation = relation.create_with(scope_for_create) if scope_for_create
end
scope = relation
end
scope = previous_scope.merge(scope) if previous_scope && action == :merge
self.current_scope = scope
begin
yield
ensure
self.current_scope = previous_scope
end
end
protected
# Works like with_scope, but discards any nested properties.
def with_exclusive_scope(method_scoping = {}, &block)
if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) }
raise ArgumentError, <<-MSG
New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope:
User.unscoped.where(:active => true)
Or call unscoped with a block:
User.unscoped do
User.where(:active => true).all
end
MSG
end
with_scope(method_scoping, :overwrite, &block)
end
def current_scope #:nodoc:
Thread.current["#{self}_current_scope"]
end
def current_scope=(scope) #:nodoc:
Thread.current["#{self}_current_scope"] = scope
end
private
def construct_finder_arel(options = {}, scope = nil)
relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options
relation = scope.merge(relation) if scope
relation
end
end
def populate_with_current_scope_attributes
return unless self.class.scope_attributes?
self.class.scope_attributes.each do |att,value|
send("#{att}=", value) if respond_to?("#{att}=")
end
end
end
end
|