methodmissing-scrooge 2.2.2 → 2.2.3

Sign up to get free protection for your applications and to get access to all the features.
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 attribute tracker to ensure production Ruby applications only fetch the database content needed to minimize wire traffic and reduce conversion overheads to native Ruby types.
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-12 14:32:45) [GET]
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
- Hotel Load (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 FROM `hotels` WHERE (`hotels`.`id` = 8699)
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
- Image Load (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 FROM `images` WHERE (`images`.`id` = 488)
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
- CACHE (0.0ms) 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 FROM `images` WHERE (`images`.`id` = 488)
30
- Address Columns (44.8ms) SHOW FIELDS FROM `addresses`
31
- Address Load Scrooged (0.5ms) SELECT `addresses`.id FROM `addresses` WHERE (`addresses`.hotel_id = 8699) LIMIT 1
32
- Rendered hotels/_show_sidebar (49.4ms)
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 56ms (View: 8, DB: 46) | 200 OK [http://localhost/hotels/8699-radisson-hotel-waterfront-cape-town]
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-12 14:32:48) [GET]
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.0ms)
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
- Address Load Scrooged (0.2ms) SELECT `addresses`.id FROM `addresses` WHERE (`addresses`.hotel_id = 8699) LIMIT 1
47
- Rendered hotels/_show_sidebar (1.3ms)
48
- Rendered shared/_footer (0.0ms)
49
- Completed in 7ms (View: 5, DB: 1) | 200 OK [http://localhost/hotels/8699-radisson-hotel-waterfront-cape-town]
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 optimization :
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
- {-113952497=>#<Set: {"User", "Password"}>}
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
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :minor: 2
3
- :patch: 2
3
+ :patch: 3
4
4
  :major: 2
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
- begin
119
- remaining_attributes = fetch_record_with_remaining_columns( columns_to_fetch )
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, Scrooge::Optimizations::Columns::SingletonMethods )
13
- ActiveRecord::Base.send( :include, Scrooge::Optimizations::Columns::InstanceMethods )
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?( Scrooge::Optimizations::Columns::InstanceMethods )
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( p_key, missing_columns )
58
- attributes = connection.send( :select, "SELECT #{scrooge_select_sql(missing_columns)} FROM #{quoted_table_name} WHERE #{quoted_table_name}.#{primary_key} = '#{p_key}'" ).first
59
- attributes ? attributes : raise( ActiveRecord::RecordNotFound )
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
- callsite_signature = (caller[ActiveRecord::Base::ScroogeCallsiteSample] << callsite_sql( sql )).hash
76
- callsite_set = scrooge_callsite(callsite_signature).columns
77
- sql = sql.gsub(scrooge_select_regex, "SELECT #{scrooge_select_sql(callsite_set)} FROM")
78
- result = connection.select_all(sanitize_sql(sql), "#{name} Load Scrooged").collect! do |record|
79
- instantiate( Scrooge::Optimizations::Columns::ScroogedAttributes.setup(record, callsite_set, self, callsite_signature) )
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
- result = connection.select_all(sanitize_sql(sql), "#{name} Load").collect! do |record|
85
- instantiate( Scrooge::Optimizations::Columns::UnscroogedAttributes.setup(record) )
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
- # Trim any conditions
97
- #
98
- def callsite_sql( sql )
99
- sql.gsub(ScroogeRegexSanitize, ScroogeBlankString)
100
- end
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?(Scrooge::Optimizations::Columns::ScroogedAttributes)
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 becomes(klass)
136
- returning klass.new do |became|
137
- became.instance_variable_set("@attributes", @attributes)
138
- became.instance_variable_set("@attributes_cache", @attributes_cache)
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,9 @@
1
+ module Scrooge
2
+ module Optimizations
3
+ module ResultSets
4
+ class ResultArray < Array
5
+ attr_accessor :unique_id
6
+ end
7
+ end
8
+ end
9
+ 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.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-16 00:00:00 -07:00
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
- - README
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