diff --git a/README.md b/README.md index c843213..007cf14 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. + ### Omission of attributes and associations You might want to exclude some attributes or associations from being dumped. For diff --git a/lib/replicate/active_record.rb b/lib/replicate/active_record.rb index 69fa824..692f3d9 100644 --- a/lib/replicate/active_record.rb +++ b/lib/replicate/active_record.rb @@ -129,12 +129,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. # @@ -175,6 +188,8 @@ def replicate_omit_attributes=(attribute_names) # 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 @@ -183,8 +198,9 @@ def replicate_omit_attributes=(attribute_names) # # 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 @@ -308,6 +324,7 @@ def disable_query_cache! ::ActiveRecord::Base.replicate_associations = [] ::ActiveRecord::Base.replicate_natural_key = [] ::ActiveRecord::Base.replicate_omit_attributes = [] + ::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 7afb04d..fb74df2 100644 --- a/test/active_record_test.rb +++ b/test/active_record_test.rb @@ -298,6 +298,62 @@ def test_loading_everything end end + def test_only_find_natural_key_on_belongs_to + 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_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] }