Browse files

Improve #fast_bootstrap speed by 50% by using the connection directly

  • Loading branch information...
1 parent f3ac4df commit 1f4c89cd5b7db2aafd02447a112d92d46a3eefac @obrie obrie committed May 1, 2009
Showing with 124 additions and 43 deletions.
  1. +2 −0 CHANGELOG.rdoc
  2. +54 −29 lib/enumerate_by.rb
  3. +68 −14 test/unit/enumerate_by_test.rb
View
2 CHANGELOG.rdoc
@@ -1,5 +1,7 @@
== master
+* Improve #fast_bootstrap speed by 50% by using the connection directly
+
== 0.4.0 / 2009-04-30
* Allow cache to be cleared on a per-enumeration basis
View
83 lib/enumerate_by.rb
@@ -261,16 +261,17 @@ def bootstrap(*records)
defaults = attributes.delete(:defaults)
# Update with new attributes
- record = !existing.include?(attributes[:id]) ? new(attributes) : begin
- record = existing[attributes[:id]]
- record.attributes = attributes
- record
- end
+ record =
+ if record = existing[attributes[:id]]
+ attributes.merge!(defaults.delete_if {|attribute, value| record.send("#{attribute}?")}) if defaults
+ record.attributes = attributes
+ record
+ else
+ attributes.merge!(defaults) if defaults
+ new(attributes)
+ end
record.id = attributes[:id]
- # Only update defaults if they aren't already specified
- defaults.each {|attribute, value| record[attribute] = value unless record.send("#{attribute}?")} if defaults
-
# Force failed saves to stop execution
record.save!
record
@@ -281,39 +282,63 @@ def bootstrap(*records)
end
# Quickly synchronizes the given records with the existing ones. This
- # disables certain features of ActiveRecord in order to provide a speed
- # boost, including:
+ # skips ActiveRecord altogether, interacting directly with the connection
+ # instead. As a result, certain features are not available when being
+ # bootstrapped, including:
# * Callbacks
# * Validations
+ # * Transactions
# * Timestamps
# * Dirty attributes
#
- # This produces a noticeable performance increase when bootstrapping more
+ # Also note that records are created directly without creating instances
+ # of the model. As a result, all of the attributes for the record must
+ # be specified.
+ #
+ # This produces a significant performance increase when bootstrapping more
# than several hundred records.
#
# See EnumerateBy::Bootstrapped#bootstrap for information about usage.
def fast_bootstrap(*records)
- features = {:callbacks => %w(create create_or_update valid?), :dirty => %w(write_attribute), :validation => %w(save save!)}
- features.each do |feature, methods|
- methods.each do |method|
- method, punctuation = method.sub(/([?!=])$/, ''), $1
- alias_method "#{method}_without_bootstrap#{punctuation}", "#{method}#{punctuation}"
- alias_method "#{method}#{punctuation}", "#{method}_without_#{feature}#{punctuation}"
- end
- end
- original_record_timestamps = self.record_timestamps
- self.record_timestamps = false
+ # Remove records that are no longer being used
+ records.flatten!
+ delete_all(['id NOT IN (?)', records.map {|record| record[:id]}])
- bootstrap(*records)
- ensure
- features.each do |feature, methods|
- methods.each do |method|
- method, punctuation = method.sub(/([?!=])$/, ''), $1
- alias_method "#{method}_without_#{feature}#{punctuation}", "#{method}#{punctuation}"
- alias_method "#{method}#{punctuation}", "#{method}_without_bootstrap#{punctuation}"
+ # Find remaining existing records (to be updated)
+ quoted_table_name = self.quoted_table_name
+ existing = connection.select_all("SELECT * FROM #{quoted_table_name}").inject({}) {|existing, record| existing[record['id'].to_i] = record; existing}
+
+ records.each do |attributes|
+ attributes.stringify_keys!
+ if defaults = attributes.delete('defaults')
+ defaults.stringify_keys!
+ end
+
+ id = attributes['id']
+ if existing_attributes = existing[id]
+ # Record exists: Update attributes
+ attributes.delete('id')
+ attributes.merge!(defaults.delete_if {|attribute, value| !existing_attributes[attribute].nil?}) if defaults
+ update_all(attributes, :id => id)
+ else
+ # Record doesn't exist: create new one
+ attributes.merge!(defaults) if defaults
+ column_names = []
+ values = []
+
+ attributes.each do |column_name, value|
+ column_names << connection.quote_column_name(column_name)
+ values << connection.quote(value, columns_hash[column_name])
+ end
+
+ connection.insert(
+ "INSERT INTO #{quoted_table_name} (#{column_names * ', '}) VALUES(#{values * ', '})",
+ "#{name} Create", primary_key, id, sequence_name
+ )
end
end
- self.record_timestamps = original_record_timestamps
+
+ true
end
end
View
82 test/unit/enumerate_by_test.rb
@@ -324,27 +324,81 @@ def test_should_update_default_attributes_if_not_defined
end
class EnumerationFastBootstrappedTest < ActiveRecord::TestCase
- def test_should_not_run_validations
- assert_raise(ActiveRecord::StatementInvalid) { Color.fast_bootstrap({:id => 1, :name => nil}) }
+ def setup
+ @result = Color.fast_bootstrap(
+ {:id => 1, :name => 'red'},
+ {:id => 2, :name => 'green'}
+ )
+ end
+
+ def test_should_not_raise_exception_if_id_not_specified
+ assert_nothing_raised { Color.fast_bootstrap({:name => 'red'}, {:name => 'green'}) }
+ assert_equal 2, Color.count
end
- def test_should_still_record_timestamps_after_bootstrap
- Color.fast_bootstrap({:id => 1, :name => 'red'})
- assert Color.record_timestamps
+ def test_should_raise_exception_if_query_fails
+ assert_raise(ActiveRecord::StatementInvalid) { Color.fast_bootstrap({:id => 1, :name => nil}, {:id => 2, :name => 'green'}) }
end
- def test_should_still_run_validations_after_bootstrap
- Color.fast_bootstrap({:id => 1, :name => 'red'})
+ def test_should_flatten_bootstrap_records
+ Color.bootstrap(
+ [{:id => 1, :name => 'red'}],
+ [{:id => 2, :name => 'green'}]
+ )
+ assert_equal 2, Color.count
+ end
+
+ def test_should_create_records
+ assert @result
+ assert_not_nil Color.find_by_name('red')
+ assert_not_nil Color.find_by_name('green')
+ end
+end
+
+class EnumeratioFastBootstrappedWithExistingRecordsTest < ActiveRecord::TestCase
+ def setup
+ @red = create_color(:name => 'RED')
+ @green = create_color(:name => 'GREEN')
+
+ Color.fast_bootstrap(
+ {:id => @red.id, :name => 'red'},
+ {:id => @green.id, :name => 'green'}
+ )
- color = Color.new
- assert !color.save
- assert_raise(ActiveRecord::RecordInvalid) { color.save! }
+ @red.reload
+ @green.reload
end
- def test_should_still_track_changed_attributes_after_bootstrap
- Color.fast_bootstrap({:id => 1, :name => 'red'})
+ def test_should_synchronize_all_attributes
+ assert_equal 'red', @red.name
+ assert_equal 'green', @green.name
+ end
+end
+
+class EnumerationFastBootstrappedWithDefaultsTest < ActiveRecord::TestCase
+ def setup
+ @red = create_color(:name => 'RED', :html => '#f00')
+ @green = create_color(:name => 'GREEN')
+
+ Color.fast_bootstrap(
+ {:id => @red.id, :name => 'red', :defaults => {:html => '#ff0000'}},
+ {:id => @green.id, :name => 'green', :defaults => {:html => '#00ff00'}}
+ )
- color = Color.new(:name => 'red')
- assert color.changed?
+ @red.reload
+ @green.reload
+ end
+
+ def test_should_update_all_non_default_attributes
+ assert_equal 'red', @red.name
+ assert_equal 'green', @green.name
+ end
+
+ def test_should_not_update_default_attributes_if_defined
+ assert_equal '#f00', @red.html
+ end
+
+ def test_should_update_default_attributes_if_not_defined
+ assert_equal '#00ff00', @green.html
end
end

0 comments on commit 1f4c89c

Please sign in to comment.