From 478228712e2639ae72e1864db4401e0a0e167f3e Mon Sep 17 00:00:00 2001 From: Sean Kirby Date: Sat, 3 Jul 2021 11:45:12 -0400 Subject: [PATCH 1/8] Allows DB lookups by arbitrary unique key at driver level --- lib/vorpal/db_loader.rb | 4 ++-- lib/vorpal/driver/postgresql.rb | 6 +++--- spec/unit/vorpal/db_loader_spec.rb | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/vorpal/db_loader.rb b/lib/vorpal/db_loader.rb index 1633680..2305f2b 100644 --- a/lib/vorpal/db_loader.rb +++ b/lib/vorpal/db_loader.rb @@ -12,7 +12,7 @@ def initialize(only_owned, db_driver) end def load_from_db(ids, config) - db_roots = @db_driver.load_by_id(config.db_class, ids) + db_roots = @db_driver.load_by_unique_key(config.db_class, ids, "id") load_from_db_objects(db_roots, config) end @@ -121,7 +121,7 @@ def initialize(config, ids) def load_all(db_driver) return [] if @ids.empty? - db_driver.load_by_id(@config.db_class, @ids) + db_driver.load_by_unique_key(@config.db_class, @ids, "id") end end diff --git a/lib/vorpal/driver/postgresql.rb b/lib/vorpal/driver/postgresql.rb index aff0b4f..faad101 100644 --- a/lib/vorpal/driver/postgresql.rb +++ b/lib/vorpal/driver/postgresql.rb @@ -40,12 +40,12 @@ def destroy(db_class, ids) db_class.where(id: ids).delete_all end - # Loads instances of the given class by primary key. + # Loads instances of the given class by a unique key. # # @param db_class [Class] A subclass of ActiveRecord::Base # @return [[Object]] An array of entities. - def load_by_id(db_class, ids) - db_class.where(id: ids).to_a + def load_by_unique_key(db_class, ids, column_name) + db_class.where(column_name => ids).to_a end # Loads instances of the given class whose foreign key has the given value. diff --git a/spec/unit/vorpal/db_loader_spec.rb b/spec/unit/vorpal/db_loader_spec.rb index 0385a02..4219cb0 100644 --- a/spec/unit/vorpal/db_loader_spec.rb +++ b/spec/unit/vorpal/db_loader_spec.rb @@ -69,8 +69,8 @@ class Comment; end best_comment_db.post_id = post_db.id driver = instance_double("Vorpal::Driver::Postgresql") - expect(driver).to receive(:load_by_id).with(PostDB, [post_db.id]).and_return([post_db]) - expect(driver).to receive(:load_by_id).with(CommentDB, [best_comment_db.id]).and_return([best_comment_db]) + expect(driver).to receive(:load_by_unique_key).with(PostDB, [post_db.id], "id").and_return([post_db]) + expect(driver).to receive(:load_by_unique_key).with(CommentDB, [best_comment_db.id], "id").and_return([best_comment_db]) expect(driver).to receive(:load_by_foreign_key).and_return([best_comment_db]) loader = Vorpal::DbLoader.new(false, driver) From 400ae09a565e7acc8bb0b1dd6b3d0c420ebd6537 Mon Sep 17 00:00:00 2001 From: Sean Kirby Date: Sat, 3 Jul 2021 15:40:05 -0400 Subject: [PATCH 2/8] Generalizes association lookup to not depend on always using ID as the unique key --- lib/vorpal/config/association_config.rb | 4 ++++ lib/vorpal/config/belongs_to_config.rb | 4 ++++ lib/vorpal/config/configs.rb | 4 ++++ lib/vorpal/config/has_many_config.rb | 4 ++++ lib/vorpal/config/has_one_config.rb | 4 ++++ lib/vorpal/db_loader.rb | 24 ++++++++++++++---------- lib/vorpal/engine.rb | 5 +++-- lib/vorpal/loaded_objects.rb | 13 +++++++++++-- lib/vorpal/util/array_hash.rb | 4 ++-- 9 files changed, 50 insertions(+), 16 deletions(-) diff --git a/lib/vorpal/config/association_config.rb b/lib/vorpal/config/association_config.rb index a99d9c2..cdebcd5 100644 --- a/lib/vorpal/config/association_config.rb +++ b/lib/vorpal/config/association_config.rb @@ -67,6 +67,10 @@ def set_foreign_key(local_db_object, remote_object) def foreign_key_info(remote_class_config) ForeignKeyInfo.new(@fk, @fk_type, remote_class_config.domain_class.name, polymorphic?) end + + def unique_key_name + (@local_end_config || @remote_end_config).unique_key_name + end end end end diff --git a/lib/vorpal/config/belongs_to_config.rb b/lib/vorpal/config/belongs_to_config.rb index 4dc93fe..144e4ef 100644 --- a/lib/vorpal/config/belongs_to_config.rb +++ b/lib/vorpal/config/belongs_to_config.rb @@ -26,6 +26,10 @@ def get_associated(owner) def associate(owner, associate) owner.send("#{name}=", associate) end + + def unique_key_name + "id" + end end end end diff --git a/lib/vorpal/config/configs.rb b/lib/vorpal/config/configs.rb index 8220cd5..973a56e 100644 --- a/lib/vorpal/config/configs.rb +++ b/lib/vorpal/config/configs.rb @@ -30,6 +30,10 @@ def set_class_config(class_config) def foreign_key_info association_config.foreign_key_info(@class_config) end + + def get_unique_key_value(db_owner) + db_owner.send(unique_key_name) + end end # @private diff --git a/lib/vorpal/config/has_many_config.rb b/lib/vorpal/config/has_many_config.rb index 9e66ad0..54621d5 100644 --- a/lib/vorpal/config/has_many_config.rb +++ b/lib/vorpal/config/has_many_config.rb @@ -29,6 +29,10 @@ def associate(owner, associates) end get_associated(owner) << associates end + + def unique_key_name + "id" + end end end end diff --git a/lib/vorpal/config/has_one_config.rb b/lib/vorpal/config/has_one_config.rb index 7656237..17ecd37 100644 --- a/lib/vorpal/config/has_one_config.rb +++ b/lib/vorpal/config/has_one_config.rb @@ -26,6 +26,10 @@ def get_associated(owner) def associate(owner, associate) owner.send("#{name}=", associate) end + + def unique_key_name + "id" + end end end end diff --git a/lib/vorpal/db_loader.rb b/lib/vorpal/db_loader.rb index 2305f2b..10676ad 100644 --- a/lib/vorpal/db_loader.rb +++ b/lib/vorpal/db_loader.rb @@ -56,15 +56,16 @@ def explore_association?(association_config) def lookup_by_id(db_object, belongs_to_config) associated_class_config = belongs_to_config.associated_class_config(db_object) - id = belongs_to_config.fk_value(db_object) - return if id.nil? || @loaded_objects.already_loaded?(associated_class_config, id) - @lookup_instructions.lookup_by_id(associated_class_config, id) + unique_key_value = belongs_to_config.fk_value(db_object) + unique_key_name = belongs_to_config.unique_key_name + return if unique_key_value.nil? || @loaded_objects.already_loaded_by_unique_key?(associated_class_config, unique_key_name, unique_key_value) + @lookup_instructions.lookup_by_unique_key(associated_class_config, unique_key_name, unique_key_value) end def lookup_by_fk(db_object, has_some_config) associated_class_config = has_some_config.associated_class_config fk_info = has_some_config.foreign_key_info - fk_value = db_object.id + fk_value = has_some_config.get_unique_key_value(db_object) @lookup_instructions.lookup_by_fk(associated_class_config, fk_info, fk_value) end end @@ -76,8 +77,8 @@ def initialize @lookup_by_fk = Util::ArrayHash.new end - def lookup_by_id(config, ids) - @lookup_by_id.append(config, ids) + def lookup_by_unique_key(config, column_name, values) + @lookup_by_id.append([config, column_name], values) end def lookup_by_fk(config, fk_info, fk_value) @@ -99,8 +100,10 @@ def empty? private def pop_id_lookup - config, ids = @lookup_by_id.pop - LookupById.new(config, ids) + key, ids = @lookup_by_id.pop + config = key.first + column_name = key.last + LookupById.new(config, column_name, ids) end def pop_fk_lookup @@ -114,14 +117,15 @@ def pop_fk_lookup # @private class LookupById attr_reader :config - def initialize(config, ids) + def initialize(config, column_name, ids) @config = config + @column_name = column_name @ids = ids end def load_all(db_driver) return [] if @ids.empty? - db_driver.load_by_unique_key(@config.db_class, @ids, "id") + db_driver.load_by_unique_key(@config.db_class, @ids, @column_name) end end diff --git a/lib/vorpal/engine.rb b/lib/vorpal/engine.rb index 2854603..d4acb48 100644 --- a/lib/vorpal/engine.rb +++ b/lib/vorpal/engine.rb @@ -137,8 +137,9 @@ def set_associations(loaded_db_objects, identity_map) loaded_db_objects.each do |config, db_objects| db_objects.each do |db_object| config.local_association_configs.each do |association_config| - db_remote = loaded_db_objects.find_by_id( + db_remote = loaded_db_objects.find_by_unique_key( association_config.remote_class_config(db_object), + association_config.unique_key_name, association_config.fk_value(db_object) ) association_config.associate(identity_map.get(db_object), identity_map.get(db_remote)) @@ -159,7 +160,7 @@ def serialize(owned_objects, mapping, loaded_db_objects) def serialize_object(object, config, loaded_db_objects) if config.serialization_required? attributes = config.serialize(object) - db_object = loaded_db_objects.find_by_id(config, object.id) + db_object = loaded_db_objects.find_by_primary_key(config, object.id) if object.id.nil? || db_object.nil? # object doesn't exist in the DB config.build_db_object(attributes) else diff --git a/lib/vorpal/loaded_objects.rb b/lib/vorpal/loaded_objects.rb index 4674039..e7a109d 100644 --- a/lib/vorpal/loaded_objects.rb +++ b/lib/vorpal/loaded_objects.rb @@ -25,16 +25,25 @@ def add(config, objects) objects_to_add end - def find_by_id(config, id) + def find_by_primary_key(config, id) @objects_by_id[[config.domain_class.name, id]] end + def find_by_unique_key(config, column_name, value) + # This linear find causes a BIG slowdown in the performance tests! Need to improve with a keyed lookup. + @objects[config].find { |object| object.send(column_name) == value } + end + def all_objects @objects_by_id.values end def already_loaded?(config, id) - !find_by_id(config, id).nil? + !find_by_primary_key(config, id).nil? + end + + def already_loaded_by_unique_key?(config, column_name, id) + !find_by_unique_key(config, column_name, id).nil? end end end diff --git a/lib/vorpal/util/array_hash.rb b/lib/vorpal/util/array_hash.rb index 0ba527d..bf3bc2c 100644 --- a/lib/vorpal/util/array_hash.rb +++ b/lib/vorpal/util/array_hash.rb @@ -6,10 +6,10 @@ module Util class ArrayHash extend Forwardable - def_delegators :@hash, :each, :empty? + def_delegators :@hash, :each, :empty?, :[] def initialize - @hash = {} + @hash = Hash.new([]) end def append(key, values) From dad9443fc1adfa0832b0df984fdd01cb49e06ca9 Mon Sep 17 00:00:00 2001 From: Sean Kirby Date: Sun, 4 Jul 2021 00:00:04 -0400 Subject: [PATCH 3/8] Restores performance to historical levels with lazy column-aware cache --- lib/vorpal/loaded_objects.rb | 49 +++++++++++++++++++++---- lib/vorpal/util/array_hash.rb | 4 ++ spec/unit/vorpal/loaded_objects_spec.rb | 35 ++++++++++++++++-- 3 files changed, 76 insertions(+), 12 deletions(-) diff --git a/lib/vorpal/loaded_objects.rb b/lib/vorpal/loaded_objects.rb index e7a109d..3d5ebfa 100644 --- a/lib/vorpal/loaded_objects.rb +++ b/lib/vorpal/loaded_objects.rb @@ -12,13 +12,13 @@ class LoadedObjects def initialize @objects = Util::ArrayHash.new - @objects_by_id = Hash.new + @cache = {} end def add(config, objects) objects_to_add = objects.map do |object| if !already_loaded?(config, object.id) - @objects_by_id[[config.domain_class.name, object.id]] = object + add_to_cache(config, object) end end.compact @objects.append(config, objects_to_add) @@ -26,24 +26,57 @@ def add(config, objects) end def find_by_primary_key(config, id) - @objects_by_id[[config.domain_class.name, id]] + find_by_unique_key(config, "id", id) end def find_by_unique_key(config, column_name, value) - # This linear find causes a BIG slowdown in the performance tests! Need to improve with a keyed lookup. - @objects[config].find { |object| object.send(column_name) == value } + get_from_cache(config, column_name, value) end def all_objects - @objects_by_id.values + @objects.values end + def already_loaded_by_unique_key?(config, column_name, id) + !find_by_unique_key(config, column_name, id).nil? + end + + private + def already_loaded?(config, id) !find_by_primary_key(config, id).nil? end - def already_loaded_by_unique_key?(config, column_name, id) - !find_by_unique_key(config, column_name, id).nil? + def add_to_cache(config, object) + # we take a shortcut here assuming that the cache has already been primed with the primary key column + # because this method should always be guarded by #already_loaded? + column_cache = @cache[config] + column_cache.each do |column_name, unique_key_cache| + unique_key_cache[object.send(column_name)] = object + end + object + end + + def get_from_cache(config, column_name, value) + lookup_hash(config, column_name)[value] + end + + # lazily primes the cache + def lookup_hash(config, column_name) + column_cache = @cache[config] + if column_cache.nil? + column_cache = {} + @cache[config] = column_cache + end + unique_key_cache = column_cache[column_name] + if unique_key_cache.nil? + unique_key_cache = {} + column_cache[column_name] = unique_key_cache + @objects[config].each do |object| + unique_key_cache[object.send(column_name)] = object + end + end + unique_key_cache end end end diff --git a/lib/vorpal/util/array_hash.rb b/lib/vorpal/util/array_hash.rb index bf3bc2c..8d50926 100644 --- a/lib/vorpal/util/array_hash.rb +++ b/lib/vorpal/util/array_hash.rb @@ -24,6 +24,10 @@ def pop values = @hash.delete(key) [key, values] end + + def values + @hash.values.flatten + end end end end diff --git a/spec/unit/vorpal/loaded_objects_spec.rb b/spec/unit/vorpal/loaded_objects_spec.rb index 00b3e09..4844292 100644 --- a/spec/unit/vorpal/loaded_objects_spec.rb +++ b/spec/unit/vorpal/loaded_objects_spec.rb @@ -7,7 +7,7 @@ let(:config) { Vorpal::Config::ClassConfig.new(domain_class: TestObject, primary_key_type: :serial) } context "#add" do - it 'does not accept duplicate objects' do + it "does not accept duplicate objects" do object = TestObject.new(id: 22) subject.add(config, [object, object]) @@ -16,7 +16,7 @@ expect(subject.all_objects).to contain_exactly(object) end - it 'returns only the objects that have not yet been encountered' do + it "returns only the objects that have not yet been encountered" do object = TestObject.new(id: 22) result = subject.add(config, [object, object]) @@ -29,11 +29,38 @@ end end + context "#find_by_unique_key" do + it "locates objects by non-primary key columns" do + object1 = TestObject.new(id: 11, unique_key: 22) + object2 = TestObject.new(id: 33, unique_key: 44) + subject.add(config, [object1, object2]) + + result = subject.find_by_unique_key(config, "unique_key", 44) + + expect(result).to eq(object2) + end + + it "locates objects by non-primary key columns even after new objects have been added" do + object1 = TestObject.new(id: 11, unique_key: 22) + subject.add(config, [object1]) + + result = subject.find_by_unique_key(config, "unique_key", 22) + expect(result).to eq(object1) + + object2 = TestObject.new(id: 33, unique_key: 44) + subject.add(config, [object2]) + + result = subject.find_by_unique_key(config, "unique_key", 44) + expect(result).to eq(object2) + end + end + class TestObject - def initialize(id:) + def initialize(id:, unique_key:nil) @id = id + @unique_key = unique_key end - attr_reader :id + attr_reader :id, :unique_key end end From 5f853a00b1ac855a8bed2aeef59d280b8d79793f Mon Sep 17 00:00:00 2001 From: Sean Kirby Date: Sun, 4 Jul 2021 14:33:27 -0400 Subject: [PATCH 4/8] Associations can now configure which column the foreign key points to --- lib/vorpal/aggregate_mapper.rb | 4 +- lib/vorpal/config/association_config.rb | 2 +- lib/vorpal/config/belongs_to_config.rb | 6 +- lib/vorpal/config/has_many_config.rb | 6 +- lib/vorpal/config/has_one_config.rb | 6 +- lib/vorpal/dsl/config_builder.rb | 3 + lib/vorpal/dsl/configuration.rb | 6 ++ lib/vorpal/engine.rb | 2 +- lib/vorpal/loaded_objects.rb | 12 ++- lib/vorpal/util/hash_initialization.rb | 2 +- .../vorpal/aggregate_mapper_spec.rb | 100 +++++++++++++++++- .../vorpal/config/association_config_spec.rb | 47 +++++++- 12 files changed, 162 insertions(+), 34 deletions(-) diff --git a/lib/vorpal/aggregate_mapper.rb b/lib/vorpal/aggregate_mapper.rb index 9c2ed27..99256c7 100644 --- a/lib/vorpal/aggregate_mapper.rb +++ b/lib/vorpal/aggregate_mapper.rb @@ -27,12 +27,12 @@ def persist(roots) # Loads an aggregate from the DB. Will eagerly load all objects in the # aggregate and on the boundary (owned: false). # - # @param db_root [Object] DB representation of the root of the aggregate to be + # @param db_root [Object, nil] DB representation of the root of the aggregate to be # loaded. This can be nil. # @param identity_map [IdentityMap] Provide your own IdentityMap instance # if you want entity id -> unique object mapping for a greater scope than one # operation. - # @return [Object] Aggregate root corresponding to the given DB representation. + # @return [Object, nil] Aggregate root corresponding to the given DB representation. def load_one(db_root, identity_map=IdentityMap.new) @engine.load_one(db_root, @domain_class, identity_map) end diff --git a/lib/vorpal/config/association_config.rb b/lib/vorpal/config/association_config.rb index cdebcd5..02050ff 100644 --- a/lib/vorpal/config/association_config.rb +++ b/lib/vorpal/config/association_config.rb @@ -59,7 +59,7 @@ def polymorphic? end def set_foreign_key(local_db_object, remote_object) - local_class_config.set_attribute(local_db_object, @fk, remote_object.try(:id)) + local_class_config.set_attribute(local_db_object, @fk, remote_object&.send(unique_key_name)) local_class_config.set_attribute(local_db_object, @fk_type, remote_object.class.name) if polymorphic? end diff --git a/lib/vorpal/config/belongs_to_config.rb b/lib/vorpal/config/belongs_to_config.rb index 144e4ef..0fe5ff7 100644 --- a/lib/vorpal/config/belongs_to_config.rb +++ b/lib/vorpal/config/belongs_to_config.rb @@ -16,7 +16,7 @@ class BelongsToConfig include Util::HashInitialization include LocalEndConfig - attr_reader :name, :owned, :fk, :fk_type, :associated_classes + attr_reader :name, :owned, :fk, :fk_type, :associated_classes, :unique_key_name attr_accessor :association_config def get_associated(owner) @@ -26,10 +26,6 @@ def get_associated(owner) def associate(owner, associate) owner.send("#{name}=", associate) end - - def unique_key_name - "id" - end end end end diff --git a/lib/vorpal/config/has_many_config.rb b/lib/vorpal/config/has_many_config.rb index 54621d5..b63c83e 100644 --- a/lib/vorpal/config/has_many_config.rb +++ b/lib/vorpal/config/has_many_config.rb @@ -16,7 +16,7 @@ class HasManyConfig include Util::HashInitialization include RemoteEndConfig - attr_reader :name, :owned, :fk, :fk_type, :associated_class + attr_reader :name, :owned, :fk, :fk_type, :associated_class, :unique_key_name attr_accessor :association_config def get_associated(owner) @@ -29,10 +29,6 @@ def associate(owner, associates) end get_associated(owner) << associates end - - def unique_key_name - "id" - end end end end diff --git a/lib/vorpal/config/has_one_config.rb b/lib/vorpal/config/has_one_config.rb index 17ecd37..8aea92d 100644 --- a/lib/vorpal/config/has_one_config.rb +++ b/lib/vorpal/config/has_one_config.rb @@ -16,7 +16,7 @@ class HasOneConfig include Util::HashInitialization include RemoteEndConfig - attr_reader :name, :owned, :fk, :fk_type, :associated_class + attr_reader :name, :owned, :fk, :fk_type, :associated_class, :unique_key_name attr_accessor :association_config def get_associated(owner) @@ -26,10 +26,6 @@ def get_associated(owner) def associate(owner, associate) owner.send("#{name}=", associate) end - - def unique_key_name - "id" - end end end end diff --git a/lib/vorpal/dsl/config_builder.rb b/lib/vorpal/dsl/config_builder.rb index c0d4840..31c5098 100644 --- a/lib/vorpal/dsl/config_builder.rb +++ b/lib/vorpal/dsl/config_builder.rb @@ -69,6 +69,7 @@ def build_class_config def build_has_many(options) options[:associated_class] ||= options[:child_class] || @defaults_generator.associated_class(options[:name]) options[:fk] ||= @defaults_generator.foreign_key(@domain_class.name) + options[:unique_key_name] ||= (options[:primary_key] || "id") options[:owned] = options.fetch(:owned, true) Vorpal::Config::HasManyConfig.new(options) end @@ -76,6 +77,7 @@ def build_has_many(options) def build_has_one(options) options[:associated_class] ||= options[:child_class] || @defaults_generator.associated_class(options[:name]) options[:fk] ||= @defaults_generator.foreign_key(@domain_class.name) + options[:unique_key_name] ||= (options[:primary_key] || "id") options[:owned] = options.fetch(:owned, true) Vorpal::Config::HasOneConfig.new(options) end @@ -84,6 +86,7 @@ def build_belongs_to(options) associated_classes = options[:associated_classes] || options[:child_classes] || options[:associated_class] || options[:child_class] || @defaults_generator.associated_class(options[:name]) options[:associated_classes] = Array(associated_classes) options[:fk] ||= @defaults_generator.foreign_key(options[:name]) + options[:unique_key_name] ||= (options[:primary_key] || "id") options[:owned] = options.fetch(:owned, true) Vorpal::Config::BelongsToConfig.new(options) end diff --git a/lib/vorpal/dsl/configuration.rb b/lib/vorpal/dsl/configuration.rb index 3b2bd93..384058c 100644 --- a/lib/vorpal/dsl/configuration.rb +++ b/lib/vorpal/dsl/configuration.rb @@ -98,6 +98,8 @@ def attributes(*attributes) # @option options [Boolean] :owned (True) True if the associated type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted. # @option options [String] :fk (Association-owning class name converted to snakecase and appended with a '_id') The name of the DB column on the associated table that contains the foreign key reference to the association owner. # @option options [String] :fk_type The name of the DB column on the associated table that contains the association-owning class name. Only needed when the associated end is polymorphic. + # @option options [String] :unique_key_name ("id") The name of the column on the owning table that the foreign key points to. Normally the primary key column. + # @option options [String] :primary_key Same as :unique_key_name. Exists for compatibility with Rails API. # @option options [Class] :child_class DEPRECATED. Use `associated_class` instead. The associated class. # @option options [Class] :associated_class (Name of the association converted to a Class) The associated class. def has_many(name, options={}) @@ -116,6 +118,8 @@ def has_many(name, options={}) # @option options [Boolean] :owned (True) True if the associated type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted. # @option options [String] :fk (Association-owning class name converted to snakecase and appended with a '_id') The name of the DB column on the associated table that contains the foreign key reference to the association owner. # @option options [String] :fk_type The name of the DB column on the associated table that contains the association-owning class name. Only needed when the associated end is polymorphic. + # @option options [String] :unique_key_name ("id") The name of the column on the owning table that the foreign key points to. Normally the primary key column. + # @option options [String] :primary_key Same as :unique_key_name. Exists for compatibility with Rails API. # @option options [Class] :child_class DEPRECATED. Use `associated_class` instead. The associated class. # @option options [Class] :associated_class (Name of the association converted to a Class) The associated class. def has_one(name, options={}) @@ -136,6 +140,8 @@ def has_one(name, options={}) # @option options [Boolean] :owned (True) True if the associated type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted. # @option options [String] :fk (Associated class name converted to snakecase and appended with a '_id') The name of the DB column on the association-owning table that contains the foreign key reference to the associated table. # @option options [String] :fk_type The name of the DB column on the association-owning table that contains the associated class name. Only needed when the association is polymorphic. + # @option options [String] :unique_key_name ("id") The name of the column on the associated table that the foreign key points to. Normally the primary key column. + # @option options [String] :primary_key Same as :unique_key_name. Exists for compatibility with Rails API. # @option options [Class] :child_class DEPRECATED. Use `associated_class` instead. The associated class. # @option options [Class] :associated_class (Name of the association converted to a Class) The associated class. # @option options [[Class]] :child_classes DEPRECATED. Use `associated_classes` instead. The list of possible classes that can be associated. This is for polymorphic associations. Takes precedence over `:associated_class`. diff --git a/lib/vorpal/engine.rb b/lib/vorpal/engine.rb index d4acb48..8280cb8 100644 --- a/lib/vorpal/engine.rb +++ b/lib/vorpal/engine.rb @@ -160,7 +160,7 @@ def serialize(owned_objects, mapping, loaded_db_objects) def serialize_object(object, config, loaded_db_objects) if config.serialization_required? attributes = config.serialize(object) - db_object = loaded_db_objects.find_by_primary_key(config, object.id) + db_object = loaded_db_objects.find_by_primary_key(config, object) if object.id.nil? || db_object.nil? # object doesn't exist in the DB config.build_db_object(attributes) else diff --git a/lib/vorpal/loaded_objects.rb b/lib/vorpal/loaded_objects.rb index 3d5ebfa..7ba6233 100644 --- a/lib/vorpal/loaded_objects.rb +++ b/lib/vorpal/loaded_objects.rb @@ -17,7 +17,7 @@ def initialize def add(config, objects) objects_to_add = objects.map do |object| - if !already_loaded?(config, object.id) + if !already_loaded?(config, object) add_to_cache(config, object) end end.compact @@ -25,8 +25,8 @@ def add(config, objects) objects_to_add end - def find_by_primary_key(config, id) - find_by_unique_key(config, "id", id) + def find_by_primary_key(config, object) + find_by_unique_key(config, "id", object.id) end def find_by_unique_key(config, column_name, value) @@ -43,10 +43,11 @@ def already_loaded_by_unique_key?(config, column_name, id) private - def already_loaded?(config, id) - !find_by_primary_key(config, id).nil? + def already_loaded?(config, object) + !find_by_primary_key(config, object).nil? end + # TODO: Do we have to worry about symbols vs strings for the column_name? def add_to_cache(config, object) # we take a shortcut here assuming that the cache has already been primed with the primary key column # because this method should always be guarded by #already_loaded? @@ -62,6 +63,7 @@ def get_from_cache(config, column_name, value) end # lazily primes the cache + # TODO: Do we have to worry about symbols vs strings for the column_name? def lookup_hash(config, column_name) column_cache = @cache[config] if column_cache.nil? diff --git a/lib/vorpal/util/hash_initialization.rb b/lib/vorpal/util/hash_initialization.rb index 2f0bebf..754da5e 100644 --- a/lib/vorpal/util/hash_initialization.rb +++ b/lib/vorpal/util/hash_initialization.rb @@ -2,7 +2,7 @@ module Vorpal module Util # @private module HashInitialization - def initialize(attrs) + def initialize(attrs={}) attrs.each do |k,v| instance_variable_set("@#{k}", v) end diff --git a/spec/acceptance/vorpal/aggregate_mapper_spec.rb b/spec/acceptance/vorpal/aggregate_mapper_spec.rb index 06411ca..6445ea6 100644 --- a/spec/acceptance/vorpal/aggregate_mapper_spec.rb +++ b/spec/acceptance/vorpal/aggregate_mapper_spec.rb @@ -15,12 +15,13 @@ def initialize(id: nil, name: "", lives_on: nil) class Trunk attr_accessor :id + attr_accessor :trunk_unique_key attr_accessor :length attr_accessor :bugs attr_accessor :tree - def initialize(id: nil, length: 0, bugs: [], tree: nil) - @id, @length, @bugs, @tree = id, length, bugs, tree + def initialize(id: nil, trunk_unique_key: nil, length: 0, bugs: [], tree: nil) + @id, @trunk_unique_key, @length, @bugs, @tree = id, trunk_unique_key, length, bugs, tree end end @@ -42,14 +43,16 @@ class Swamp < ActiveRecord::Base; end class Tree attr_accessor :id attr_accessor :name + attr_accessor :tree_unique_key attr_accessor :trunk attr_accessor :environment attr_accessor :fissures attr_accessor :branches - def initialize(id: nil, name: "", trunk: nil, environment: nil, fissures: [], branches: []) + def initialize(id: nil, name: "", tree_unique_key: nil, trunk: nil, environment: nil, fissures: [], branches: []) @id = id @name = name + @tree_unique_key = tree_unique_key @trunk = trunk @environment = environment @fissures = fissures @@ -61,8 +64,8 @@ def initialize(id: nil, name: "", trunk: nil, environment: nil, fissures: [], br define_table('branches', {length: :decimal, tree_id: :integer, branch_id: :integer}, false) define_table('bugs', {name: :text, lives_on_id: :integer, lives_on_type: :string}, false) define_table('fissures', {length: :decimal, tree_id: :integer}, false) - define_table('trees', {name: :text, trunk_id: :integer, environment_id: :integer, environment_type: :string}, false) - define_table('trunks', {length: :decimal}, false) + define_table('trees', {name: :text, tree_unique_key: :integer, trunk_id: :integer, environment_id: :integer, environment_type: :string}, false) + define_table('trunks', {trunk_unique_key: :integer, length: :decimal}, false) define_table('swamps', {}, false) end @@ -777,6 +780,73 @@ def initialize(id: nil, name: "") end end + describe 'associations using non-primary keys' do + describe 'has_one' do + let(:test_mapper) do + engine = Vorpal.define do + map Tree do + attributes :name + end + + map Trunk do + attributes :trunk_unique_key + has_one :tree, primary_key: :trunk_unique_key + end + end + engine.mapper_for(Trunk) + end + + it 'sets foreign keys' do + tree = Tree.new + trunk = Trunk.new(trunk_unique_key: 1111, tree: tree) + + test_mapper.persist(trunk) + + tree_db = db_class_for(Tree, test_mapper).first + expect(tree_db.trunk_id).to eq 1111 + end + + it 'hydrates' do + trunk_db = db_class_for(Trunk, test_mapper).create!(trunk_unique_key: 1111, length: 9090) + db_class_for(Tree, test_mapper).create!(trunk_id: 1111, name: 'big tree') + + trunk = test_mapper.load_one(trunk_db) + + expect(trunk.tree.name).to eq('big tree') + end + end + + it 'sets foreign keys' do + test_mapper = configure_unique_key_associations + trunk = Trunk.new(trunk_unique_key: 1111) + tree = Tree.new(tree_unique_key: 2222, trunk: trunk) + trunk.tree = tree + tree.branches = [Branch.new, Branch.new] + + test_mapper.persist(tree) + + tree_db = db_class_for(Tree, test_mapper).first + expect(tree_db.trunk_id).to eq 1111 + + branch_db_1 = db_class_for(Branch, test_mapper).first + expect(branch_db_1.tree_id).to eq(2222) + branch_db_2 = db_class_for(Branch, test_mapper).last + expect(branch_db_2.tree_id).to eq(2222) + end + + it 'hydrates' do + test_mapper = configure_unique_key_associations + db_class_for(Trunk, test_mapper).create!(trunk_unique_key: 1111, length: 9090) + db_class_for(Branch, test_mapper).create!(tree_id: 2222, length: 8080) + tree_db = db_class_for(Tree, test_mapper).create!(trunk_id: 1111, tree_unique_key: 2222) + + tree = test_mapper.load_one(tree_db) + + expect(tree.trunk.length).to eq 9090 + expect(tree.branches).to match_array([an_object_having_attributes(length: 8080)]) + end + end + private def configure_uuid_id @@ -963,4 +1033,24 @@ def configure_unowned_has_one end engine.mapper_for(Trunk) end + + def configure_unique_key_associations + engine = Vorpal.define do + map Tree do + attributes :name, :tree_unique_key + belongs_to :trunk, primary_key: :trunk_unique_key + has_many :branches, primary_key: :tree_unique_key + end + + map Trunk do + attributes :length, :trunk_unique_key + has_one :tree, primary_key: :trunk_unique_key + end + + map Branch do + attributes :length + end + end + engine.mapper_for(Tree) + end end diff --git a/spec/unit/vorpal/config/association_config_spec.rb b/spec/unit/vorpal/config/association_config_spec.rb index ad03995..5c6b960 100644 --- a/spec/unit/vorpal/config/association_config_spec.rb +++ b/spec/unit/vorpal/config/association_config_spec.rb @@ -1,12 +1,15 @@ require 'unit_spec_helper' require 'vorpal/config/main_config' require 'vorpal/config/class_config' +require 'vorpal/config/has_many_config' +require 'vorpal/config/has_one_config' +require 'vorpal/config/belongs_to_config' module ConfigsSpec describe Vorpal::Config::AssociationConfig do class Post - attr_accessor :comments - attr_accessor :best_comment + include Vorpal::Util::HashInitialization + attr_accessor :id, :comments, :best_comment, :post_unique_key end class Comment @@ -19,7 +22,7 @@ class Comment let(:post_has_one_comment_config) { Vorpal::Config::HasOneConfig.new(name: 'best_comment', fk: 'post_id', associated_class: Comment) } let(:comment_belongs_to_post_config) { Vorpal::Config::BelongsToConfig.new(name: 'post', fk: 'post_id', associated_classes: [Post]) } - describe 'associate' do + describe '#associate' do let(:post) { Post.new } let(:comment) { Comment.new } @@ -50,7 +53,43 @@ class Comment end end - describe 'remote_class_config' do + describe '#set_foreign_key' do + class CommentDB + attr_accessor :post_id + end + + it 'sets the foreign key column from the remote object primary key' do + config = Vorpal::Config::AssociationConfig.new(comment_config, 'post_id', nil) + comment_belongs_to_post_config = Vorpal::Config::BelongsToConfig.new( + name: 'post', unique_key_name: 'id', fk: 'post_id', associated_classes: [Post] + ) + config.local_end_config = comment_belongs_to_post_config + + post = Post.new(id: 1111) + comment_db = CommentDB.new + + config.set_foreign_key(comment_db, post) + + expect(comment_db.post_id).to eq(1111) + end + + it 'sets the foreign key column from the remote object unique key' do + config = Vorpal::Config::AssociationConfig.new(comment_config, 'post_id', nil) + comment_belongs_to_post_config = Vorpal::Config::BelongsToConfig.new( + name: 'post', unique_key_name: 'post_unique_key', fk: 'post_id', associated_classes: [Post] + ) + config.local_end_config = comment_belongs_to_post_config + + post = Post.new(id: 1111, post_unique_key: 2222) + comment_db = CommentDB.new + + config.set_foreign_key(comment_db, post) + + expect(comment_db.post_id).to eq(2222) + end + end + + describe '#remote_class_config' do it 'works with non-polymorphic associations' do config = Vorpal::Config::AssociationConfig.new(comment_config, 'post_id', nil) config.add_remote_class_config(post_config) From 862e96d3b3392aba896fc92cf78b977579d71e6d Mon Sep 17 00:00:00 2001 From: Sean Kirby Date: Sun, 4 Jul 2021 22:06:14 -0400 Subject: [PATCH 5/8] Fix bug where has_one associations could not be cleared --- lib/vorpal/engine.rb | 2 +- .../vorpal/aggregate_mapper_spec.rb | 92 +++++++++++++------ 2 files changed, 67 insertions(+), 27 deletions(-) diff --git a/lib/vorpal/engine.rb b/lib/vorpal/engine.rb index 8280cb8..7611540 100644 --- a/lib/vorpal/engine.rb +++ b/lib/vorpal/engine.rb @@ -203,7 +203,7 @@ def set_foreign_keys(owned_objects, mapping) config.has_ones.each do |has_one_config| if has_one_config.owned associate = has_one_config.get_associated(object) - has_one_config.set_foreign_key(mapping[associate], object) + has_one_config.set_foreign_key(mapping[associate], object) if associate end end diff --git a/spec/acceptance/vorpal/aggregate_mapper_spec.rb b/spec/acceptance/vorpal/aggregate_mapper_spec.rb index 6445ea6..4607bf0 100644 --- a/spec/acceptance/vorpal/aggregate_mapper_spec.rb +++ b/spec/acceptance/vorpal/aggregate_mapper_spec.rb @@ -176,32 +176,6 @@ def initialize(id: nil, name: "", tree_unique_key: nil, trunk: nil, environment: expect(db_class_for(Tree, test_mapper).count).to eq 1 end - - it 'removes orphans' do - test_mapper = configure - - tree_db = db_class_for(Tree, test_mapper).create! - db_class_for(Branch, test_mapper).create!(tree_id: tree_db.id) - - tree = Tree.new(id: tree_db.id, branches: []) - - test_mapper.persist(tree) - - expect(db_class_for(Branch, test_mapper).count).to eq 0 - end - - it 'does not remove orphans from unowned associations' do - test_mapper = configure_unowned - - tree_db = db_class_for(Tree, test_mapper).create! - db_class_for(Branch, test_mapper).create!(tree_id: tree_db.id) - - tree = Tree.new(id: tree_db.id, branches: []) - - test_mapper.persist(tree) - - expect(db_class_for(Branch, test_mapper).count).to eq 1 - end end it 'copies attributes to domain' do @@ -332,6 +306,28 @@ def initialize(id: nil, name: "", tree_unique_key: nil, trunk: nil, environment: new_tree = test_mapper.load_one(tree_db) expect(new_tree.trunk.length).to eq 21 end + + it 'removes orphans' do + test_mapper = configure + trunk_db = db_class_for(Trunk, test_mapper).create! + tree_db = db_class_for(Tree, test_mapper).create!(trunk_id: trunk_db.id) + tree = Tree.new(id: tree_db.id, trunk: nil) + + test_mapper.persist(tree) + + expect(db_class_for(Trunk, test_mapper).count).to eq(0) + end + + it 'does not remove orphans when unowned' do + test_mapper = configure_unowned + trunk_db = db_class_for(Trunk, test_mapper).create! + tree_db = db_class_for(Tree, test_mapper).create!(trunk_id: trunk_db.id) + tree = Tree.new(id: tree_db.id, trunk: nil) + + test_mapper.persist(tree) + + expect(db_class_for(Tree, test_mapper).count).to eq(1) + end end describe 'has_many associations' do @@ -398,6 +394,28 @@ def initialize(id: nil, name: "", tree_unique_key: nil, trunk: nil, environment: expect(tree.branches.first.length).to eq 50 end + + it 'removes orphans' do + test_mapper = configure + tree_db = db_class_for(Tree, test_mapper).create! + db_class_for(Branch, test_mapper).create!(tree_id: tree_db.id) + tree = Tree.new(id: tree_db.id, branches: []) + + test_mapper.persist(tree) + + expect(db_class_for(Branch, test_mapper).count).to eq 0 + end + + it 'does not remove orphans when unowned' do + test_mapper = configure_unowned + tree_db = db_class_for(Tree, test_mapper).create! + db_class_for(Branch, test_mapper).create!(tree_id: tree_db.id) + tree = Tree.new(id: tree_db.id, branches: []) + + test_mapper.persist(tree) + + expect(db_class_for(Branch, test_mapper).count).to eq 1 + end end describe 'has_one associations' do @@ -441,6 +459,28 @@ def initialize(id: nil, name: "", tree_unique_key: nil, trunk: nil, environment: expect(trunk.tree.name).to eq 'big tree' end + + it 'removes orphans' do + test_mapper = configure_has_one + trunk_db = db_class_for(Trunk, test_mapper).create! + db_class_for(Tree, test_mapper).create!(trunk_id: trunk_db.id) + trunk = Trunk.new(id: trunk_db.id) + + test_mapper.persist(trunk) + + expect(db_class_for(Tree, test_mapper).count).to eq(0) + end + + it 'does not remove orphans when unowned' do + test_mapper = configure_unowned_has_one + trunk_db = db_class_for(Trunk, test_mapper).create! + db_class_for(Tree, test_mapper).create!(trunk_id: trunk_db.id) + trunk = Trunk.new(id: trunk_db.id) + + test_mapper.persist(trunk) + + expect(db_class_for(Tree, test_mapper).count).to eq(1) + end end describe 'polymorphic associations' do From c85f59778d392b4935d7300ef9990db96b634bd2 Mon Sep 17 00:00:00 2001 From: Sean Kirby Date: Sun, 4 Jul 2021 22:31:15 -0400 Subject: [PATCH 6/8] Enforce that both ends of an association must have the same unique_key_name --- lib/vorpal/config/association_config.rb | 8 +++++ lib/vorpal/config/belongs_to_config.rb | 4 +++ lib/vorpal/config/has_many_config.rb | 4 +++ lib/vorpal/config/has_one_config.rb | 4 +++ lib/vorpal/config/main_config.rb | 1 + lib/vorpal/exceptions.rb | 2 ++ .../vorpal/aggregate_mapper_spec.rb | 30 +++++++++++++++++++ 7 files changed, 53 insertions(+) diff --git a/lib/vorpal/config/association_config.rb b/lib/vorpal/config/association_config.rb index 02050ff..48644d9 100644 --- a/lib/vorpal/config/association_config.rb +++ b/lib/vorpal/config/association_config.rb @@ -71,6 +71,14 @@ def foreign_key_info(remote_class_config) def unique_key_name (@local_end_config || @remote_end_config).unique_key_name end + + def validate + if @local_end_config && @remote_end_config + if @local_end_config.unique_key_name != @remote_end_config.unique_key_name + raise ConfigurationError.new("#{@local_end_config.pretty_name} and #{@remote_end_config.pretty_name} must have the same unique_key_name/primary_key") + end + end + end end end end diff --git a/lib/vorpal/config/belongs_to_config.rb b/lib/vorpal/config/belongs_to_config.rb index 0fe5ff7..0802f0c 100644 --- a/lib/vorpal/config/belongs_to_config.rb +++ b/lib/vorpal/config/belongs_to_config.rb @@ -26,6 +26,10 @@ def get_associated(owner) def associate(owner, associate) owner.send("#{name}=", associate) end + + def pretty_name + "#{association_config.local_class_config.domain_class.name} belongs_to :#{name}" + end end end end diff --git a/lib/vorpal/config/has_many_config.rb b/lib/vorpal/config/has_many_config.rb index b63c83e..c6b9d5d 100644 --- a/lib/vorpal/config/has_many_config.rb +++ b/lib/vorpal/config/has_many_config.rb @@ -29,6 +29,10 @@ def associate(owner, associates) end get_associated(owner) << associates end + + def pretty_name + "#{@class_config.domain_class.name} has_many :#{name}" + end end end end diff --git a/lib/vorpal/config/has_one_config.rb b/lib/vorpal/config/has_one_config.rb index 8aea92d..a29d205 100644 --- a/lib/vorpal/config/has_one_config.rb +++ b/lib/vorpal/config/has_one_config.rb @@ -26,6 +26,10 @@ def get_associated(owner) def associate(owner, associate) owner.send("#{name}=", associate) end + + def pretty_name + "#{@class_config.domain_class.name} has_one :#{name}" + end end end end diff --git a/lib/vorpal/config/main_config.rb b/lib/vorpal/config/main_config.rb index cb4ab91..0ea6428 100644 --- a/lib/vorpal/config/main_config.rb +++ b/lib/vorpal/config/main_config.rb @@ -48,6 +48,7 @@ def initialize_association_configs association_configs.values.each do |association_config| association_config.local_class_config.local_association_configs << association_config + association_config.validate end end diff --git a/lib/vorpal/exceptions.rb b/lib/vorpal/exceptions.rb index db57b19..a13755b 100644 --- a/lib/vorpal/exceptions.rb +++ b/lib/vorpal/exceptions.rb @@ -4,4 +4,6 @@ class InvalidPrimaryKeyValue < StandardError; end class InvalidAggregateRoot < StandardError; end class ConfigurationNotFound < StandardError; end + + class ConfigurationError < StandardError; end end diff --git a/spec/acceptance/vorpal/aggregate_mapper_spec.rb b/spec/acceptance/vorpal/aggregate_mapper_spec.rb index 4607bf0..1ff8cc7 100644 --- a/spec/acceptance/vorpal/aggregate_mapper_spec.rb +++ b/spec/acceptance/vorpal/aggregate_mapper_spec.rb @@ -794,6 +794,36 @@ def initialize(id: nil, name: "", tree_unique_key: nil, trunk: nil, environment: end end + describe 'mis-configured' do + it 'raises an error when both sides of a 1-1 association do not have the same unique_key_name set' do + expect { + Vorpal.define do + map Tree do + belongs_to :trunk, primary_key: :id + end + + map Trunk do + has_one :tree, primary_key: :trunk_unique_key + end + end + }.to raise_error(Vorpal::ConfigurationError, "Tree belongs_to :trunk and Trunk has_one :tree must have the same unique_key_name/primary_key") + end + + it 'raises an error when both sides of a 1-* association do not have the same unique_key_name set' do + expect { + Vorpal.define do + map Tree do + has_many :branches, primary_key: :id + end + + map Branch do + belongs_to :tree, primary_key: :tree_unique_key + end + end + }.to raise_error(Vorpal::ConfigurationError, "Branch belongs_to :tree and Tree has_many :branches must have the same unique_key_name/primary_key") + end + end + class TreeUUID attr_accessor :id attr_accessor :name From 94ce57a56149ea2a0e60781c06dea48d2b0d1af7 Mon Sep 17 00:00:00 2001 From: Sean Kirby Date: Wed, 7 Jul 2021 13:30:25 -0400 Subject: [PATCH 7/8] Raise a nice error when a has_many is set to nil instead of [] --- lib/vorpal/aggregate_traversal.rb | 1 + lib/vorpal/exceptions.rb | 2 ++ spec/acceptance/vorpal/aggregate_mapper_spec.rb | 9 +++++++++ 3 files changed, 12 insertions(+) diff --git a/lib/vorpal/aggregate_traversal.rb b/lib/vorpal/aggregate_traversal.rb index 7c1eaa9..9186826 100644 --- a/lib/vorpal/aggregate_traversal.rb +++ b/lib/vorpal/aggregate_traversal.rb @@ -30,6 +30,7 @@ def accept(object, visitor, already_visited=[]) config.has_manys.each do |has_many_config| associates = has_many_config.get_associated(object) + raise InvariantViolated.new("#{has_many_config.pretty_name} was set to nil. Use an empty array instead.") if associates.nil? associates.each do |associate| accept(associate, visitor, already_visited) if visitor.continue_traversal?(has_many_config) end diff --git a/lib/vorpal/exceptions.rb b/lib/vorpal/exceptions.rb index a13755b..f486b03 100644 --- a/lib/vorpal/exceptions.rb +++ b/lib/vorpal/exceptions.rb @@ -6,4 +6,6 @@ class InvalidAggregateRoot < StandardError; end class ConfigurationNotFound < StandardError; end class ConfigurationError < StandardError; end + + class InvariantViolated < StandardError; end end diff --git a/spec/acceptance/vorpal/aggregate_mapper_spec.rb b/spec/acceptance/vorpal/aggregate_mapper_spec.rb index 1ff8cc7..7ed870f 100644 --- a/spec/acceptance/vorpal/aggregate_mapper_spec.rb +++ b/spec/acceptance/vorpal/aggregate_mapper_spec.rb @@ -416,6 +416,15 @@ def initialize(id: nil, name: "", tree_unique_key: nil, trunk: nil, environment: expect(db_class_for(Branch, test_mapper).count).to eq 1 end + + it 'raise a nice error when set to nil' do + test_mapper = configure + tree = Tree.new(branches: nil) + + expect { + test_mapper.persist(tree) + }.to raise_error(Vorpal::InvariantViolated, "Tree has_many :branches was set to nil. Use an empty array instead.") + end end describe 'has_one associations' do From 0659ff172371b4f715c4afe278ea312cf91f3678 Mon Sep 17 00:00:00 2001 From: Sean Kirby Date: Wed, 7 Jul 2021 13:54:19 -0400 Subject: [PATCH 8/8] Make sure we are testing the :unique_key_name option --- spec/acceptance/vorpal/aggregate_mapper_spec.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/spec/acceptance/vorpal/aggregate_mapper_spec.rb b/spec/acceptance/vorpal/aggregate_mapper_spec.rb index 7ed870f..161c9e0 100644 --- a/spec/acceptance/vorpal/aggregate_mapper_spec.rb +++ b/spec/acceptance/vorpal/aggregate_mapper_spec.rb @@ -808,11 +808,11 @@ def initialize(id: nil, name: "", tree_unique_key: nil, trunk: nil, environment: expect { Vorpal.define do map Tree do - belongs_to :trunk, primary_key: :id + belongs_to :trunk, unique_key_name: :id end map Trunk do - has_one :tree, primary_key: :trunk_unique_key + has_one :tree, unique_key_name: :trunk_unique_key end end }.to raise_error(Vorpal::ConfigurationError, "Tree belongs_to :trunk and Trunk has_one :tree must have the same unique_key_name/primary_key") @@ -822,11 +822,11 @@ def initialize(id: nil, name: "", tree_unique_key: nil, trunk: nil, environment: expect { Vorpal.define do map Tree do - has_many :branches, primary_key: :id + has_many :branches, unique_key_name: :id end map Branch do - belongs_to :tree, primary_key: :tree_unique_key + belongs_to :tree, unique_key_name: :tree_unique_key end end }.to raise_error(Vorpal::ConfigurationError, "Branch belongs_to :tree and Tree has_many :branches must have the same unique_key_name/primary_key") @@ -868,8 +868,7 @@ def initialize(id: nil, name: "") end map Trunk do - attributes :trunk_unique_key - has_one :tree, primary_key: :trunk_unique_key + has_one :tree, unique_key_name: :trunk_unique_key end end engine.mapper_for(Trunk)