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 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