methodmissing-scrooge 2.2.2 → 2.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.textile +27 -20
- data/VERSION.yml +1 -1
- data/lib/callsite.rb +19 -1
- data/lib/optimizations/associations/macro.rb +129 -0
- data/lib/optimizations/columns/attributes_proxy.rb +19 -14
- data/lib/optimizations/columns/macro.rb +49 -40
- data/lib/optimizations/result_sets/result_array.rb +9 -0
- data/lib/optimizations/result_sets/updateable_result_set.rb +85 -0
- data/lib/scrooge.rb +22 -5
- metadata +9 -5
data/README.textile
CHANGED
@@ -4,49 +4,51 @@ h4. This is a complete rewrite from the initial coverage at "igvita.com":http://
|
|
4
4
|
|
5
5
|
Many thanks to Stephen Sykes ( "pennysmalls.com":http://pennysmalls.com ) for his time spent on shaping, implementing and troubleshooting this release.
|
6
6
|
|
7
|
-
An ActiveRecord
|
7
|
+
An ActiveRecord optimization layer to ensure production Ruby applications only fetch the database content needed to minimize wire traffic, excessive SQL queries and reduce conversion overheads to native Ruby types.
|
8
8
|
|
9
9
|
h2. Why bother ?
|
10
10
|
|
11
11
|
* Object conversion and moving unnecessary data is both expensive and tax existing infrastructure in high load setups
|
12
12
|
* Manually extracting and scoping SELECT clauses is not sustainable in a clean and painless manner with iterative development, even less so in large projects.
|
13
|
+
* Preloading associations can be painful - delegate to Scrooge instead.
|
13
14
|
|
14
15
|
h2. What it does
|
15
16
|
|
16
17
|
<pre>
|
17
18
|
<code>
|
18
|
-
Processing HotelsController#show (for 127.0.0.1 at 2009-03-
|
19
|
+
Processing HotelsController#show (for 127.0.0.1 at 2009-03-18 19:29:38) [GET]
|
19
20
|
Parameters: {"action"=>"show", "id"=>"8699-radisson-hotel-waterfront-cape-town", "controller"=>"hotels"}
|
20
21
|
Hotel Load Scrooged (0.3ms) SELECT `hotels`.id FROM `hotels` WHERE (`hotels`.`id` = 8699)
|
21
22
|
Rendering template within layouts/application
|
22
23
|
Rendering hotels/show
|
23
|
-
|
24
|
+
SQL (0.2ms) SELECT `hotels`.location_id,`hotels`.hotel_name,`hotels`.location,`hotels`.from_price,`hotels`.star_rating,`hotels`.apt,`hotels`.latitude,`hotels`.longitude,`hotels`.distance,`hotels`.narrative,`hotels`.telephone,`hotels`.important_notes,`hotels`.nearest_tube,`hotels`.nearest_rail,`hotels`.created_at,`hotels`.updated_at,`hotels`.id FROM `hotels` WHERE `hotels`.id IN ('8699')
|
24
25
|
Image Load Scrooged (0.2ms) SELECT `images`.id FROM `images` WHERE (`images`.hotel_id = 8699) LIMIT 1
|
25
|
-
|
26
|
+
SQL (0.2ms) SELECT `images`.hotel_id,`images`.title,`images`.url,`images`.width,`images`.height,`images`.thumbnail_url,`images`.thumbnail_width,`images`.thumbnail_height,`images`.has_thumbnail,`images`.created_at,`images`.updated_at,`images`.id FROM `images` WHERE `images`.id IN ('488')
|
26
27
|
Rendered shared/_header (0.0ms)
|
27
28
|
Rendered shared/_navigation (0.2ms)
|
28
29
|
Image Load Scrooged (0.2ms) SELECT `images`.id FROM `images` WHERE (`images`.hotel_id = 8699)
|
29
|
-
|
30
|
-
Address Columns (
|
31
|
-
Address Load Scrooged (
|
32
|
-
Rendered hotels/_show_sidebar (
|
30
|
+
SQL (0.2ms) SELECT `images`.hotel_id,`images`.title,`images`.url,`images`.width,`images`.height,`images`.thumbnail_url,`images`.thumbnail_width,`images`.thumbnail_height,`images`.has_thumbnail,`images`.created_at,`images`.updated_at,`images`.id FROM `images` WHERE `images`.id IN ('488')
|
31
|
+
Address Columns (306.2ms) SHOW FIELDS FROM `addresses`
|
32
|
+
Address Load Scrooged (3.6ms) SELECT `addresses`.id FROM `addresses` WHERE (`addresses`.hotel_id = 8699) LIMIT 1
|
33
|
+
Rendered hotels/_show_sidebar (313.2ms)
|
33
34
|
Rendered shared/_footer (0.1ms)
|
34
|
-
Completed in
|
35
|
+
Completed in 320ms (View: 8, DB: 311) | 200 OK [http://localhost/hotels/8699-radisson-hotel-waterfront-cape-town]
|
35
36
|
|
36
37
|
|
37
|
-
Processing HotelsController#show (for 127.0.0.1 at 2009-03-
|
38
|
+
Processing HotelsController#show (for 127.0.0.1 at 2009-03-18 19:29:40) [GET]
|
38
39
|
Parameters: {"action"=>"show", "id"=>"8699-radisson-hotel-waterfront-cape-town", "controller"=>"hotels"}
|
39
40
|
Hotel Load Scrooged (0.3ms) SELECT `hotels`.narrative,`hotels`.from_price,`hotels`.star_rating,`hotels`.hotel_name,`hotels`.id FROM `hotels` WHERE (`hotels`.`id` = 8699)
|
41
|
+
Address Load Scrooged (0.2ms) SELECT `addresses`.id FROM `addresses` WHERE (`addresses`.hotel_id = 8699)
|
40
42
|
Rendering template within layouts/application
|
41
43
|
Rendering hotels/show
|
42
44
|
Image Load Scrooged (0.3ms) SELECT `images`.url,`images`.id,`images`.height,`images`.width FROM `images` WHERE (`images`.hotel_id = 8699) LIMIT 1
|
43
|
-
Rendered shared/_header (0.
|
45
|
+
Rendered shared/_header (0.1ms)
|
44
46
|
Rendered shared/_navigation (0.2ms)
|
45
47
|
Image Load Scrooged (0.3ms) SELECT `images`.thumbnail_width,`images`.id,`images`.thumbnail_height,`images`.thumbnail_url FROM `images` WHERE (`images`.hotel_id = 8699)
|
46
|
-
|
47
|
-
Rendered
|
48
|
-
|
49
|
-
|
48
|
+
Rendered hotels/_show_sidebar (1.0ms)
|
49
|
+
Rendered shared/_footer (0.1ms)
|
50
|
+
Completed in 8ms (View: 5, DB: 1) | 200 OK [http://localhost/hotels/8699-radisson-hotel-waterfront-cape-town]
|
51
|
+
|
50
52
|
</code>
|
51
53
|
</pre>
|
52
54
|
|
@@ -184,7 +186,7 @@ Callsites are tracked on a per model ( table name ) basis.
|
|
184
186
|
|
185
187
|
h4. Scope
|
186
188
|
|
187
|
-
Only SQL statements that meet the following criteria is considered for
|
189
|
+
Only SQL statements that meet the following criteria is considered for column optimizations :
|
188
190
|
|
189
191
|
* A SELECT statement
|
190
192
|
|
@@ -192,9 +194,16 @@ Only SQL statements that meet the following criteria is considered for optimizat
|
|
192
194
|
|
193
195
|
* The Model has a primary key defined
|
194
196
|
|
197
|
+
Only associations that meet the following criteria is associated with a callsite and preloaded
|
198
|
+
on subsequent requests :
|
199
|
+
|
200
|
+
* Not a polymorphic association
|
201
|
+
|
202
|
+
* Not a collection ( has_many etc. )
|
203
|
+
|
195
204
|
h4. How it tracks
|
196
205
|
|
197
|
-
The ActiveRecord attributes Hash is replaced with a proxy that automatically augments the callsite with any attributes referenced through the Hash lookup keys.
|
206
|
+
The ActiveRecord attributes Hash is replaced with a proxy that automatically augments the callsite with any attributes referenced through the Hash lookup keys.We're also able to learn which associations is invoked from a given callsite, for preloading on subsequent requests.
|
198
207
|
|
199
208
|
h4. Storage
|
200
209
|
|
@@ -202,7 +211,7 @@ There's a slight memory hit for each model as the callsites is stored as a class
|
|
202
211
|
|
203
212
|
<pre>
|
204
213
|
<code>
|
205
|
-
|
214
|
+
#<Scrooge::Callsite:0x3969968 @primary_key="id", @columns=#<Set: {"narrative", "from_price", "star_rating", "hotel_name", "id"}>, @associations=#<Set: {:address}>, @signature=-736202783, @klass=Hotel(id: integer, location_id: integer, hotel_name: string, location: string, from_price: float, star_rating: integer, apt: boolean, latitude: float, longitude: float, distance: float, narrative: text, telephone: string, important_notes: text, nearest_tube: string, nearest_rail: string, created_at: datetime, updated_at: datetime), @inheritance_column="type">
|
206
215
|
</code>
|
207
216
|
</pre>
|
208
217
|
|
@@ -216,8 +225,6 @@ h2. Todo
|
|
216
225
|
|
217
226
|
* Test cases for Scrooge::Callsite
|
218
227
|
|
219
|
-
* Track both columns AND association invocations off Scrooge::Callsite
|
220
|
-
|
221
228
|
* Have invoking Model#attributes not associate all columns with the callsite
|
222
229
|
|
223
230
|
* Avoid possible missing attribute exceptions for destroyed objects
|
data/VERSION.yml
CHANGED
data/lib/callsite.rb
CHANGED
@@ -27,16 +27,34 @@ module Scrooge
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
+
# Diff known associations with given includes
|
31
|
+
#
|
32
|
+
def preload( includes )
|
33
|
+
# Ignore nested includes for the time being
|
34
|
+
#
|
35
|
+
if includes.is_a?(Hash)
|
36
|
+
includes
|
37
|
+
else
|
38
|
+
@associations.merge( Array(includes) ).to_a
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
30
42
|
# Flag an association as seen
|
31
43
|
#
|
32
44
|
def association!( association )
|
33
45
|
Mutex.synchronize do
|
34
|
-
@associations << association
|
46
|
+
@associations << association if preloadable_association?( association )
|
35
47
|
end
|
36
48
|
end
|
37
49
|
|
38
50
|
private
|
39
51
|
|
52
|
+
# Only register associations that isn't polymorphic or a collection
|
53
|
+
#
|
54
|
+
def preloadable_association?( association )
|
55
|
+
@klass.preloadable_associations.include?( association.to_sym )
|
56
|
+
end
|
57
|
+
|
40
58
|
# Is the table a container for STI models ?
|
41
59
|
#
|
42
60
|
def inheritable?
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Scrooge
|
2
|
+
module Optimizations
|
3
|
+
module Associations
|
4
|
+
module Macro
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
# Inject into ActiveRecord
|
9
|
+
#
|
10
|
+
def install!
|
11
|
+
if scrooge_installable?
|
12
|
+
ActiveRecord::Base.send( :extend, SingletonMethods )
|
13
|
+
ActiveRecord::Base.send( :include, InstanceMethods )
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def scrooge_installable?
|
20
|
+
!ActiveRecord::Base.included_modules.include?( InstanceMethods )
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
module SingletonMethods
|
28
|
+
|
29
|
+
@@preloadable_associations = {}
|
30
|
+
FindAssociatedRegex = /find_associated_records/
|
31
|
+
|
32
|
+
def self.extended( base )
|
33
|
+
eigen = class << base; self; end
|
34
|
+
eigen.instance_eval do
|
35
|
+
# Let :scrooge_callsite be a valid find option
|
36
|
+
#
|
37
|
+
remove_const(:VALID_FIND_OPTIONS)
|
38
|
+
const_set( :VALID_FIND_OPTIONS, [ :conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group, :having, :from, :lock, :scrooge_callsite ] )
|
39
|
+
end
|
40
|
+
eigen.alias_method_chain :find, :scrooge
|
41
|
+
eigen.alias_method_chain :find_every, :scrooge
|
42
|
+
end
|
43
|
+
|
44
|
+
# Let .find setup callsite information and preloading.
|
45
|
+
#
|
46
|
+
def find_with_scrooge(*args)
|
47
|
+
options = args.extract_options!
|
48
|
+
validate_find_options(options)
|
49
|
+
set_readonly_option!(options)
|
50
|
+
|
51
|
+
if (_caller = caller).grep( FindAssociatedRegex ).empty?
|
52
|
+
cs_signature = callsite_signature( _caller, options.except(:conditions, :limit, :offset) )
|
53
|
+
options[:scrooge_callsite], options[:include] = cs_signature, scrooge_callsite(cs_signature).preload( options[:include] )
|
54
|
+
end
|
55
|
+
|
56
|
+
case args.first
|
57
|
+
when :first then find_initial(options)
|
58
|
+
when :last then find_last(options)
|
59
|
+
when :all then find_every(options)
|
60
|
+
else find_from_ids(args, options)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Override find_ever to pass along the callsite signature
|
65
|
+
#
|
66
|
+
def find_every_with_scrooge(options)
|
67
|
+
include_associations = merge_includes(scope(:find, :include), options[:include])
|
68
|
+
|
69
|
+
if include_associations.any? && references_eager_loaded_tables?(options)
|
70
|
+
records = find_with_associations(options)
|
71
|
+
else
|
72
|
+
records = find_by_sql(construct_finder_sql(options), options[:scrooge_callsite])
|
73
|
+
if include_associations.any?
|
74
|
+
preload_associations(records, include_associations)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
records.each { |record| record.readonly! } if options[:readonly]
|
79
|
+
|
80
|
+
records
|
81
|
+
end
|
82
|
+
|
83
|
+
# Let's not preload polymorphic associations or collections
|
84
|
+
#
|
85
|
+
def preloadable_associations
|
86
|
+
@@preloadable_associations[self.name] ||= reflect_on_all_associations.reject{|a| a.options[:polymorphic] || a.macro == :has_many }.map{|a| a.name }
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
module InstanceMethods
|
92
|
+
|
93
|
+
# Association getter with Scrooge support
|
94
|
+
#
|
95
|
+
def association_instance_get(name)
|
96
|
+
association = instance_variable_get("@#{name}")
|
97
|
+
if association.respond_to?(:loaded?)
|
98
|
+
scrooge_seen_association!( name )
|
99
|
+
association
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Association setter with Scrooge support
|
104
|
+
#
|
105
|
+
def association_instance_set(name, association)
|
106
|
+
scrooge_seen_association!( name )
|
107
|
+
instance_variable_set("@#{name}", association)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Register an association with Scrooge
|
111
|
+
#
|
112
|
+
def scrooge_seen_association!( association )
|
113
|
+
if scrooged? && !scrooge_seen_association?( association )
|
114
|
+
@attributes.scrooge_associations << association
|
115
|
+
self.class.scrooge_callsite( @attributes.callsite_signature ).association!( association )
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def scrooge_seen_association?( association )
|
122
|
+
@attributes.scrooge_associations.include?( association )
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -18,6 +18,12 @@ module Scrooge
|
|
18
18
|
end
|
19
19
|
|
20
20
|
alias_method :merge!, :update
|
21
|
+
|
22
|
+
# Don't try to reload one of these
|
23
|
+
#
|
24
|
+
def fully_fetched
|
25
|
+
true
|
26
|
+
end
|
21
27
|
end
|
22
28
|
|
23
29
|
class ScroogedAttributes < Hash
|
@@ -25,14 +31,16 @@ module Scrooge
|
|
25
31
|
# Hash container for attributes with scrooge monitoring of attribute access
|
26
32
|
#
|
27
33
|
|
28
|
-
attr_accessor :callsite_signature, :scrooge_columns, :fully_fetched, :klass
|
34
|
+
attr_accessor :callsite_signature, :scrooge_columns, :scrooge_associations, :fully_fetched, :klass, :updateable_result_set
|
29
35
|
|
30
|
-
def self.setup(record, scrooge_columns, klass, callsite_signature)
|
36
|
+
def self.setup(record, scrooge_columns, scrooge_associations, klass, callsite_signature, updateable_result_set)
|
31
37
|
hash = new.replace(record)
|
32
38
|
hash.scrooge_columns = scrooge_columns.dup
|
39
|
+
hash.scrooge_associations = scrooge_associations.dup
|
33
40
|
hash.fully_fetched = false
|
34
41
|
hash.klass = klass
|
35
42
|
hash.callsite_signature = callsite_signature
|
43
|
+
hash.updateable_result_set = updateable_result_set
|
36
44
|
hash
|
37
45
|
end
|
38
46
|
|
@@ -115,18 +123,10 @@ module Scrooge
|
|
115
123
|
protected
|
116
124
|
|
117
125
|
def fetch_remaining!( columns_to_fetch )
|
118
|
-
|
119
|
-
|
120
|
-
rescue ActiveRecord::RecordNotFound
|
121
|
-
raise ActiveRecord::MissingAttributeError, "scrooge cannot fetch missing attribute(s) #{columns_to_fetch.to_a.join(', ')} because record went away"
|
122
|
-
end
|
123
|
-
replace(remaining_attributes.merge(self))
|
126
|
+
@updateable_result_set.updaters_attributes = self # for after_initialize & after_find
|
127
|
+
@updateable_result_set.reload_columns!(columns_to_fetch)
|
124
128
|
end
|
125
|
-
|
126
|
-
def fetch_record_with_remaining_columns( columns_to_fetch )
|
127
|
-
@klass.scrooge_reload(self[@klass.primary_key], columns_to_fetch)
|
128
|
-
end
|
129
|
-
|
129
|
+
|
130
130
|
def interesting_for_scrooge?( attr_s )
|
131
131
|
has_key?(attr_s) && !@scrooge_columns.include?(attr_s)
|
132
132
|
end
|
@@ -135,11 +135,16 @@ module Scrooge
|
|
135
135
|
@klass.scrooge_seen_column!(callsite_signature, attr_s)
|
136
136
|
end
|
137
137
|
|
138
|
+
def primary_key_name
|
139
|
+
@klass.primary_key
|
140
|
+
end
|
141
|
+
|
138
142
|
def dup_self
|
139
143
|
@scrooge_columns = @scrooge_columns.dup
|
144
|
+
@scrooge_associations = @scrooge_associations.dup
|
140
145
|
self
|
141
146
|
end
|
142
147
|
end
|
143
148
|
end
|
144
149
|
end
|
145
|
-
end
|
150
|
+
end
|
@@ -9,15 +9,15 @@ module Scrooge
|
|
9
9
|
#
|
10
10
|
def install!
|
11
11
|
if scrooge_installable?
|
12
|
-
ActiveRecord::Base.send( :extend,
|
13
|
-
ActiveRecord::Base.send( :include,
|
12
|
+
ActiveRecord::Base.send( :extend, SingletonMethods )
|
13
|
+
ActiveRecord::Base.send( :include, InstanceMethods )
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
17
|
private
|
18
18
|
|
19
19
|
def scrooge_installable?
|
20
|
-
!ActiveRecord::Base.included_modules.include?(
|
20
|
+
!ActiveRecord::Base.included_modules.include?( InstanceMethods )
|
21
21
|
end
|
22
22
|
|
23
23
|
end
|
@@ -54,9 +54,9 @@ module Scrooge
|
|
54
54
|
# Efficient reloading - get the hash with missing attributes directly from the
|
55
55
|
# underlying connection.
|
56
56
|
#
|
57
|
-
def scrooge_reload(
|
58
|
-
|
59
|
-
|
57
|
+
def scrooge_reload( p_keys, missing_columns )
|
58
|
+
sql_keys = p_keys.collect{|pk| "'#{pk}'"}.join(ScroogeComma)
|
59
|
+
connection.send( :select, "SELECT #{scrooge_select_sql(missing_columns)} FROM #{quoted_table_name} WHERE #{quoted_table_name}.#{primary_key} IN (#{sql_keys})" )
|
60
60
|
end
|
61
61
|
|
62
62
|
private
|
@@ -71,34 +71,39 @@ module Scrooge
|
|
71
71
|
|
72
72
|
# Find through callsites.
|
73
73
|
#
|
74
|
-
def find_by_sql_with_scrooge( sql )
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
74
|
+
def find_by_sql_with_scrooge( sql, callsite_sig = nil )
|
75
|
+
callsite_sig ||= callsite_signature( caller, callsite_sql( sql ) )
|
76
|
+
callsite_columns = scrooge_callsite(callsite_sig).columns
|
77
|
+
callsite_associations = scrooge_callsite(callsite_sig).associations
|
78
|
+
sql = sql.gsub(scrooge_select_regex, "SELECT #{scrooge_select_sql(callsite_columns)} FROM")
|
79
|
+
results = connection.select_all(sanitize_sql(sql), "#{name} Load Scrooged")
|
80
|
+
result_set = ResultSets::ResultArray.new
|
81
|
+
updateable = ResultSets::UpdateableResultSet.new(result_set, self)
|
82
|
+
results.inject(result_set) do |memo, record|
|
83
|
+
memo << instantiate(ScroogedAttributes.setup(record, callsite_columns, callsite_associations, self, callsite_sig, updateable))
|
80
84
|
end
|
81
|
-
end
|
85
|
+
end
|
82
86
|
|
83
|
-
def find_by_sql_without_scrooge( sql )
|
84
|
-
|
85
|
-
|
87
|
+
def find_by_sql_without_scrooge( sql, callsite = nil )
|
88
|
+
scrooge_unlink_callsite!( callsite ) if callsite
|
89
|
+
connection.select_all(sanitize_sql(sql), "#{name} Load").collect! do |record|
|
90
|
+
instantiate( UnscroogedAttributes.setup(record) )
|
86
91
|
end
|
87
92
|
end
|
88
|
-
|
89
|
-
# Generate a regex that respects the table name as well to catch
|
90
|
-
# verbose SQL from JOINS etc.
|
91
|
-
#
|
92
|
-
def scrooge_select_regex
|
93
|
-
@@scrooge_select_regexes[self.table_name] ||= Regexp.compile( "SELECT (`?(?:#{table_name})?`?.?\\*) FROM" )
|
94
|
-
end
|
95
93
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
94
|
+
# Generate a regex that respects the table name as well to catch
|
95
|
+
# verbose SQL from JOINS etc.
|
96
|
+
#
|
97
|
+
def scrooge_select_regex
|
98
|
+
@@scrooge_select_regexes[self.table_name] ||= Regexp.compile( "SELECT (`?(?:#{table_name})?`?.?\\*) FROM" )
|
99
|
+
end
|
100
|
+
|
101
|
+
# Trim any conditions
|
102
|
+
#
|
103
|
+
def callsite_sql( sql )
|
104
|
+
sql.gsub(ScroogeRegexSanitize, ScroogeBlankString)
|
105
|
+
end
|
106
|
+
|
102
107
|
end
|
103
108
|
|
104
109
|
module InstanceMethods
|
@@ -108,12 +113,13 @@ module Scrooge
|
|
108
113
|
base.alias_method_chain :destroy, :scrooge
|
109
114
|
base.alias_method_chain :respond_to?, :scrooge
|
110
115
|
base.alias_method_chain :attributes_from_column_definition, :scrooge
|
116
|
+
base.alias_method_chain :becomes, :scrooge
|
111
117
|
end
|
112
118
|
|
113
119
|
# Is this instance being handled by scrooge?
|
114
120
|
#
|
115
121
|
def scrooged?
|
116
|
-
@attributes.is_a?(
|
122
|
+
@attributes.is_a?(ScroogedAttributes)
|
117
123
|
end
|
118
124
|
|
119
125
|
# Delete should fully load all the attributes before the @attributes hash is frozen
|
@@ -132,17 +138,13 @@ module Scrooge
|
|
132
138
|
|
133
139
|
# Augment callsite info for new model class when using STI
|
134
140
|
#
|
135
|
-
def
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
became.instance_variable_set("@new_record", new_record?)
|
140
|
-
if scrooged?
|
141
|
-
self.class.scrooge_callsite(@attributes.callsite_signature).columns.each do |attrib|
|
142
|
-
became.class.scrooge_seen_column!(@attributes.callsite_signature, attrib)
|
143
|
-
end
|
141
|
+
def becomes_with_scrooge(klass)
|
142
|
+
if scrooged?
|
143
|
+
self.class.scrooge_callsite(@attributes.callsite_signature).columns.each do |attrib|
|
144
|
+
klass.scrooge_seen_column!(@attributes.callsite_signature, attrib)
|
144
145
|
end
|
145
146
|
end
|
147
|
+
becomes_without_scrooge(klass)
|
146
148
|
end
|
147
149
|
|
148
150
|
# Marshal
|
@@ -150,6 +152,7 @@ module Scrooge
|
|
150
152
|
#
|
151
153
|
def _dump(depth)
|
152
154
|
scrooge_fetch_remaining
|
155
|
+
scrooge_invalidate_updateable_result_set
|
153
156
|
scrooge_dump_flag_this
|
154
157
|
str = Marshal.dump(self)
|
155
158
|
scrooge_dump_unflag_this
|
@@ -194,6 +197,12 @@ module Scrooge
|
|
194
197
|
@attributes.fetch_remaining if scrooged?
|
195
198
|
end
|
196
199
|
|
200
|
+
# Dumped objects should not contain object_ids of old result sets
|
201
|
+
#
|
202
|
+
def scrooge_invalidate_updateable_result_set
|
203
|
+
@attributes.updateable_result_set = ResultSets::UpdateableResultSet.new(nil, self) if scrooged?
|
204
|
+
end
|
205
|
+
|
197
206
|
# New objects should get an UnscroogedAttributes as their @attributes hash
|
198
207
|
#
|
199
208
|
def attributes_from_column_definition_with_scrooge
|
@@ -204,4 +213,4 @@ module Scrooge
|
|
204
213
|
|
205
214
|
end
|
206
215
|
end
|
207
|
-
end
|
216
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Scrooge
|
2
|
+
module Optimizations
|
3
|
+
module ResultSets
|
4
|
+
class UpdateableResultSet
|
5
|
+
|
6
|
+
# Contains a weak referernce to the result set, and can update from DB
|
7
|
+
#
|
8
|
+
|
9
|
+
attr_accessor :updaters_attributes
|
10
|
+
|
11
|
+
def initialize(result_set_array, klass)
|
12
|
+
if result_set_array
|
13
|
+
@result_set_object_id = result_set_array.object_id
|
14
|
+
@unique_id = result_set_array.unique_id ||= "#{Time.now.to_f}#{object_id}" # avoid recycled object ids
|
15
|
+
end
|
16
|
+
@klass = klass # expected class of items in the array
|
17
|
+
end
|
18
|
+
|
19
|
+
def reload_columns!(columns_to_fetch)
|
20
|
+
reloaded_columns = hash_by_primary_key(reload_columns_for_ids(columns_to_fetch, result_set_ids))
|
21
|
+
if !reloaded_columns.has_key?(@updaters_attributes[primary_key_name])
|
22
|
+
raise ActiveRecord::MissingAttributeError, "scrooge cannot fetch missing attribute(s) #{columns_to_fetch.to_a.join(', ')} because record went away"
|
23
|
+
else
|
24
|
+
update_with(reloaded_columns)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def reload_columns_for_ids(columns_to_fetch, result_ids_to_fetch)
|
29
|
+
@klass.scrooge_reload(result_ids_to_fetch, columns_to_fetch + [primary_key_name])
|
30
|
+
end
|
31
|
+
|
32
|
+
def result_set_attributes
|
33
|
+
rs = result_set
|
34
|
+
return default_attributes unless rs
|
35
|
+
rs.inject(default_attributes) do |memo, r|
|
36
|
+
if r.is_a?(@klass)
|
37
|
+
memo << r.instance_variable_get(:@attributes)
|
38
|
+
end
|
39
|
+
memo
|
40
|
+
end.uniq
|
41
|
+
end
|
42
|
+
|
43
|
+
def result_set
|
44
|
+
return nil unless @result_set_object_id
|
45
|
+
result_set = ObjectSpace._id2ref(@result_set_object_id)
|
46
|
+
result_set.is_a?(ResultArray) && result_set.unique_id == @unique_id ? result_set : nil
|
47
|
+
rescue RangeError
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def default_attributes
|
52
|
+
[@updaters_attributes]
|
53
|
+
end
|
54
|
+
|
55
|
+
def result_set_ids
|
56
|
+
result_set_attributes.inject([]) do |memo, attributes|
|
57
|
+
unless attributes.fully_fetched
|
58
|
+
memo << attributes[primary_key_name]
|
59
|
+
end
|
60
|
+
memo
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def update_with(remaining_attributes)
|
65
|
+
current_attributes = hash_by_primary_key(result_set_attributes)
|
66
|
+
remaining_attributes.each do |r_id, r_att|
|
67
|
+
old_attributes = current_attributes[r_id]
|
68
|
+
if old_attributes
|
69
|
+
old_attributes.update(r_att.merge(old_attributes))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def hash_by_primary_key(rows)
|
75
|
+
rows.inject({}) {|memo, row| memo[row[primary_key_name]] = row; memo}
|
76
|
+
end
|
77
|
+
|
78
|
+
def primary_key_name
|
79
|
+
@klass.primary_key
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/scrooge.rb
CHANGED
@@ -4,6 +4,9 @@ require 'set'
|
|
4
4
|
require 'callsite'
|
5
5
|
require 'optimizations/columns/attributes_proxy'
|
6
6
|
require 'optimizations/columns/macro'
|
7
|
+
require 'optimizations/associations/macro'
|
8
|
+
require 'optimizations/result_sets/updateable_result_set'
|
9
|
+
require 'optimizations/result_sets/result_array'
|
7
10
|
|
8
11
|
module ActiveRecord
|
9
12
|
class Base
|
@@ -12,15 +15,15 @@ module ActiveRecord
|
|
12
15
|
ScroogeCallsiteSample = 0..10
|
13
16
|
|
14
17
|
class << self
|
15
|
-
|
18
|
+
|
16
19
|
# Determine if a given SQL string is a candidate for callsite <=> columns
|
17
20
|
# optimization.
|
18
21
|
#
|
19
|
-
def find_by_sql(sql)
|
22
|
+
def find_by_sql(sql, callsite_signature = nil)
|
20
23
|
if scope_with_scrooge?(sql)
|
21
|
-
find_by_sql_with_scrooge(sql)
|
24
|
+
find_by_sql_with_scrooge(sql, callsite_signature)
|
22
25
|
else
|
23
|
-
find_by_sql_without_scrooge(sql)
|
26
|
+
find_by_sql_without_scrooge(sql, callsite_signature)
|
24
27
|
end
|
25
28
|
end
|
26
29
|
|
@@ -45,6 +48,12 @@ module ActiveRecord
|
|
45
48
|
|
46
49
|
private
|
47
50
|
|
51
|
+
# Removes a single callsite
|
52
|
+
#
|
53
|
+
def scrooge_unlink_callsite!( callsite_signature )
|
54
|
+
@@scrooge_callsites.delete(callsite_signature)
|
55
|
+
end
|
56
|
+
|
48
57
|
# Initialize a callsite
|
49
58
|
#
|
50
59
|
def callsite( signature )
|
@@ -56,10 +65,18 @@ module ActiveRecord
|
|
56
65
|
def attribute_with_table( attr_name )
|
57
66
|
"#{quoted_table_name}.#{attr_name.to_s}"
|
58
67
|
end
|
68
|
+
|
69
|
+
# Computes a unique signature from a given call stack and supplementary
|
70
|
+
# context information.
|
71
|
+
#
|
72
|
+
def callsite_signature( call_stack, supplementary )
|
73
|
+
( call_stack[ScroogeCallsiteSample] << supplementary ).hash
|
74
|
+
end
|
59
75
|
|
60
76
|
end # class << self
|
61
77
|
|
62
78
|
end
|
63
79
|
end
|
64
80
|
|
65
|
-
Scrooge::Optimizations::Columns::Macro.install!
|
81
|
+
Scrooge::Optimizations::Columns::Macro.install!
|
82
|
+
Scrooge::Optimizations::Associations::Macro.install!
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: methodmissing-scrooge
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.2.
|
4
|
+
version: 2.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- "Lourens Naud\xC3\xA9"
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2009-03-
|
13
|
+
date: 2009-03-18 00:00:00 -07:00
|
14
14
|
default_executable:
|
15
15
|
dependencies: []
|
16
16
|
|
@@ -20,9 +20,8 @@ executables: []
|
|
20
20
|
|
21
21
|
extensions: []
|
22
22
|
|
23
|
-
extra_rdoc_files:
|
24
|
-
|
25
|
-
- README.textile
|
23
|
+
extra_rdoc_files: []
|
24
|
+
|
26
25
|
files:
|
27
26
|
- Rakefile
|
28
27
|
- README
|
@@ -30,9 +29,14 @@ files:
|
|
30
29
|
- VERSION.yml
|
31
30
|
- lib/callsite.rb
|
32
31
|
- lib/optimizations
|
32
|
+
- lib/optimizations/associations
|
33
|
+
- lib/optimizations/associations/macro.rb
|
33
34
|
- lib/optimizations/columns
|
34
35
|
- lib/optimizations/columns/attributes_proxy.rb
|
35
36
|
- lib/optimizations/columns/macro.rb
|
37
|
+
- lib/optimizations/result_sets
|
38
|
+
- lib/optimizations/result_sets/result_array.rb
|
39
|
+
- lib/optimizations/result_sets/updateable_result_set.rb
|
36
40
|
- lib/scrooge.rb
|
37
41
|
- test/helper.rb
|
38
42
|
- test/models
|