Skip to content

Commit

Permalink
Merge branch 'associations-using-non-pk-columns'
Browse files Browse the repository at this point in the history
  • Loading branch information
sskirby committed Jul 7, 2021
2 parents 12fd795 + 0659ff1 commit 9bbc402
Show file tree
Hide file tree
Showing 21 changed files with 405 additions and 75 deletions.
4 changes: 2 additions & 2 deletions lib/vorpal/aggregate_mapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/vorpal/aggregate_traversal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion lib/vorpal/config/association_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,26 @@ 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

# @return ForeignKeyInfo
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

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
6 changes: 5 additions & 1 deletion lib/vorpal/config/belongs_to_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
4 changes: 4 additions & 0 deletions lib/vorpal/config/configs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion lib/vorpal/config/has_many_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
6 changes: 5 additions & 1 deletion lib/vorpal/config/has_one_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
1 change: 1 addition & 0 deletions lib/vorpal/config/main_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 15 additions & 11 deletions lib/vorpal/db_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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_id(@config.db_class, @ids)
db_driver.load_by_unique_key(@config.db_class, @ids, @column_name)
end
end

Expand Down
6 changes: 3 additions & 3 deletions lib/vorpal/driver/postgresql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions lib/vorpal/dsl/config_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ 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

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
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions lib/vorpal/dsl/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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={})
Expand All @@ -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={})
Expand All @@ -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`.
Expand Down
7 changes: 4 additions & 3 deletions lib/vorpal/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)
if object.id.nil? || db_object.nil? # object doesn't exist in the DB
config.build_db_object(attributes)
else
Expand Down Expand Up @@ -202,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

Expand Down
4 changes: 4 additions & 0 deletions lib/vorpal/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ class InvalidPrimaryKeyValue < StandardError; end
class InvalidAggregateRoot < StandardError; end

class ConfigurationNotFound < StandardError; end

class ConfigurationError < StandardError; end

class InvariantViolated < StandardError; end
end
60 changes: 52 additions & 8 deletions lib/vorpal/loaded_objects.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,73 @@ 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
if !already_loaded?(config, object)
add_to_cache(config, object)
end
end.compact
@objects.append(config, objects_to_add)
objects_to_add
end

def find_by_id(config, id)
@objects_by_id[[config.domain_class.name, 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)
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, 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?
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 already_loaded?(config, id)
!find_by_id(config, id).nil?
def get_from_cache(config, column_name, value)
lookup_hash(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?
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
Loading

0 comments on commit 9bbc402

Please sign in to comment.