From 91db67e8e35c3bc3118aebf04ed68fe1a342d83e Mon Sep 17 00:00:00 2001 From: Martin Tepper Date: Thu, 22 Sep 2011 11:34:19 +0200 Subject: [PATCH 1/2] Implementing an option to only find objects with natural keys. --- README.md | 14 ++++++++++++++ lib/replicate/active_record.rb | 21 +++++++++++++++++++-- test/active_record_test.rb | 31 +++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 86fde0b..efcc35a 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,20 @@ end Multiple attribute names may be specified to define a compound key. Foreign key column attributes (`user_id`) are often included in natural keys. +You can also use the :only_find option to disable creation or updating of unfound +objects entirely: + +```ruby +class EmailAddress < ActiveRecord::Base + belongs_to :user + replicate_natural_key :user_id, :email, :only_find => true +end +``` + +In this example, loading a dump of users with email addresses will result in all +users loaded, but they will have only the email addresses that are already in the +target system. + ### Validations and Callbacks __IMPORTANT:__ All ActiveRecord validations and callbacks are disabled on the diff --git a/lib/replicate/active_record.rb b/lib/replicate/active_record.rb index 5ecf81a..d80988d 100644 --- a/lib/replicate/active_record.rb +++ b/lib/replicate/active_record.rb @@ -125,12 +125,25 @@ def replicate_associations=(names) # Compound key used during load to locate existing objects for update. # When no natural key is defined, objects are created new. # + # Use :only_find => true as an option to only find existing objects - if + # none are found, they will not be created. + # # attribute_names - Macro style setter. def replicate_natural_key(*attribute_names) + options = attribute_names.last.is_a?(Hash) ? attribute_names.pop : {} self.replicate_natural_key = attribute_names if attribute_names.any? + self.only_find_natural_key = options[:only_find] if options.has_key?(:only_find) @replicate_natural_key || superclass.replicate_natural_key end + def only_find_natural_key + @only_find_natural_key + end + + def only_find_natural_key=(only_find) + @only_find_natural_key = only_find + end + # Set the compound key used to locate existing objects for update when # loading. When not set, loading will always create new records. # @@ -154,6 +167,8 @@ def replicate_id=(boolean) # Load an individual record into the database. If the models defines a # replicate_natural_key then an existing record will be updated if found # instead of a new record being created. + # If the :only_find option is set to true on replicate_natural_key, will + # not create a new record if it isn't found. # # type - Model class name as a String. # id - Primary key id of the record on the dump system. This must be @@ -162,8 +177,9 @@ def replicate_id=(boolean) # # Returns the ActiveRecord object instance for the new record. def load_replicant(type, id, attributes) - instance = replicate_find_existing_record(attributes) || new - create_or_update_replicant instance, attributes + instance = replicate_find_existing_record(attributes) + return if instance.nil? and only_find_natural_key + create_or_update_replicant instance || new, attributes end # Locate an existing record using the replicate_natural_key attribute @@ -286,6 +302,7 @@ def disable_query_cache! ::ActiveRecord::Base.send :extend, ClassMethods ::ActiveRecord::Base.replicate_associations = [] ::ActiveRecord::Base.replicate_natural_key = [] + ::ActiveRecord::Base.only_find_natural_key = false ::ActiveRecord::Base.replicate_id = false end end diff --git a/test/active_record_test.rb b/test/active_record_test.rb index 8fe46b1..eaea7ac 100644 --- a/test/active_record_test.rb +++ b/test/active_record_test.rb @@ -270,6 +270,37 @@ def test_loading_everything end end + def test_only_find_natural_key + objects = [] + @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } + + assert_equal(3, Profile.count) + assert_equal(3, User.count) + + %w[rtomayko kneath tmm1].each do |login| + user = User.find_by_login(login) + @dumper.dump user + end + assert_equal 6, objects.size + + User.find_by_login("kneath").profile.destroy + User.delete_all + assert_equal(0, User.count) + assert_equal(2, Profile.count) + + # We only want to reattach profiles, not recreate them + Profile.replicate_natural_key :user_id, :only_find => true + + # load everything back up + objects.each { |type, id, attrs, obj| @loader.feed type, id, attrs } + + assert_equal(3, User.count) + assert_equal(2, Profile.count) + assert_nil User.find_by_login("kneath").profile + assert_not_nil User.find_by_login("rtomayko").profile + assert_not_nil User.find_by_login("tmm1").profile + end + def test_loading_with_existing_records objects = [] @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } From 620047e96a0cd50f33619c3cd87ee2ac4dd27851 Mon Sep 17 00:00:00 2001 From: Martin Tepper Date: Fri, 23 Sep 2011 11:18:58 +0200 Subject: [PATCH 2/2] Another test, for has_many associations. --- test/active_record_test.rb | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/test/active_record_test.rb b/test/active_record_test.rb index c4a2732..fb74df2 100644 --- a/test/active_record_test.rb +++ b/test/active_record_test.rb @@ -298,7 +298,7 @@ def test_loading_everything end end - def test_only_find_natural_key + def test_only_find_natural_key_on_belongs_to objects = [] @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } @@ -329,6 +329,31 @@ def test_only_find_natural_key assert_not_nil User.find_by_login("tmm1").profile end + def test_only_find_natural_key_on_has_many + objects = [] + @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } + + User.replicate_associations :emails + Email.replicate_natural_key = [] + Email.replicate_natural_key :email, :only_find => true + + rtomayko = User.find_by_login('rtomayko') + @dumper.dump rtomayko + assert_equal 4, objects.size + + emails = rtomayko.emails + Email.destroy_all + + objects.each { |type, id, attrs, obj| @loader.feed type, id, attrs } + assert_equal(0, rtomayko.emails.count) + + email = Email.create!(:email => emails.first.email) + assert_equal(0, rtomayko.emails.count) + objects.each { |type, id, attrs, obj| @loader.feed type, id, attrs } + assert_equal(1, rtomayko.emails.count) + assert_equal(email.email, rtomayko.reload.emails.first.email) + end + def test_loading_with_existing_records objects = [] @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }