Skip to content

Commit

Permalink
Refactored Relationships to use repository name insetad of reference
Browse files Browse the repository at this point in the history
* This resolves some nasty bugs where repository objects were being
  used that weren't on the scope stack, so they would have ever-increasing
  IdentityMap, but they were inaccessible to all other code that uses
  repository blocks.
* Updated model to have a default scope, and changed Discriminator and
  Paranoid types to update the default scope with the conditions rather
  than push onto the scope stack directly.
* Removed unecessary instantiation of repository object where possible.
* Minor code cleanup
  • Loading branch information
Dan Kubb committed Jul 11, 2008
1 parent eb18f92 commit af5c773
Show file tree
Hide file tree
Showing 19 changed files with 199 additions and 164 deletions.
2 changes: 1 addition & 1 deletion lib/dm-core/associations/many_to_many.rb
Expand Up @@ -41,7 +41,7 @@ def #{name}_association
opts.delete(:through)
opts[:child_model] ||= opts.delete(:class_name) || Extlib::Inflection.classify(name)
opts[:parent_model] = model
opts[:repository] = model.repository
opts[:repository_name] = repository_name
opts[:remote_relationship_name] ||= opts.delete(:remote_name) || name
opts[:parent_key] = opts[:parent_key]
opts[:child_key] = opts[:child_key]
Expand Down
2 changes: 1 addition & 1 deletion lib/dm-core/associations/many_to_one.rb
Expand Up @@ -38,7 +38,7 @@ def #{name}_association

model.relationships(repository_name)[name] = Relationship.new(
name,
model.repository,
repository_name,
model,
options.fetch(:class_name, Extlib::Inflection.classify(name)),
options
Expand Down
6 changes: 2 additions & 4 deletions lib/dm-core/associations/one_to_many.rb
@@ -1,5 +1,3 @@
require 'forwardable'

module DataMapper
module Associations
module OneToMany
Expand Down Expand Up @@ -46,7 +44,7 @@ def #{name}_association

opts[:child_model] ||= opts.delete(:class_name) || Extlib::Inflection.classify(name)
opts[:parent_model] = model
opts[:repository] = model.repository
opts[:repository_name] = repository_name
opts[:near_relationship_name] = opts.delete(:through)
opts[:remote_relationship_name] ||= opts.delete(:remote_name) || name
opts[:parent_key] = opts[:parent_key]
Expand All @@ -56,7 +54,7 @@ def #{name}_association
else
Relationship.new(
name,
model.repository,
repository_name,
options.fetch(:class_name, Extlib::Inflection.classify(name)),
model,
options
Expand Down
4 changes: 2 additions & 2 deletions lib/dm-core/associations/one_to_one.rb
Expand Up @@ -40,7 +40,7 @@ def #{name}_association
RelationshipChain.new(
:child_model => options.fetch(:class_name, Extlib::Inflection.classify(name)),
:parent_model => model,
:repository => model.repository,
:repository_name => repository_name,
:near_relationship_name => options[:through],
:remote_relationship_name => options.fetch(:remote_name, name),
:parent_key => options[:parent_key],
Expand All @@ -49,7 +49,7 @@ def #{name}_association
else
Relationship.new(
name,
model.repository,
repository_name,
options.fetch(:class_name, Extlib::Inflection.classify(name)),
model,
options
Expand Down
127 changes: 70 additions & 57 deletions lib/dm-core/associations/relationship.rb
Expand Up @@ -5,13 +5,15 @@ class Relationship

OPTIONS = [ :class_name, :child_key, :parent_key, :min, :max, :through ]

attr_reader :name, :repository, :options, :query
# @api private
attr_reader :name, :options, :query

# @api private
def child_key
@child_key ||= begin
child_key = nil
with_repository(child_model) do
model_properties = child_model.properties
child_model.repository.scope do |r|
model_properties = child_model.properties(r.name)

child_key = parent_key.zip(@child_properties || []).map do |parent_property,property_name|
# TODO: use something similar to DM::NamingConventions to determine the property name
Expand All @@ -21,32 +23,33 @@ def child_key
if model_properties.has_property?(property_name)
model_properties[property_name]
else
attributes = {}
options = {}

[ :length, :precision, :scale ].each do |attribute|
attributes[attribute] = parent_property.send(attribute)
[ :length, :precision, :scale ].each do |option|
options[option] = parent_property.send(option)
end

# NOTE: hack to make each many to many child_key a true key,
# until I can figure out a better place for this check
if child_model.respond_to?(:many_to_many)
attributes[:key] = true
options[:key] = true
end

child_model.property(property_name, parent_property.primitive, attributes)
child_model.property(property_name, parent_property.primitive, options)
end
end
end
PropertySet.new(child_key)
end
end

# @api private
def parent_key
@parent_key ||= begin
parent_key = nil
with_repository(parent_model) do
parent_model.repository.scope do |r|
parent_key = if @parent_properties
parent_model.properties.slice(*@parent_properties)
parent_model.properties(r.name).slice(*@parent_properties)
else
parent_model.key
end
Expand All @@ -55,33 +58,31 @@ def parent_key
end
end

# @api private
def parent_model
Class === @parent_model ? @parent_model : (Class === @child_model ? @child_model.find_const(@parent_model) : Object.find_const(@parent_model))
end

# @api private
def child_model
Class === @child_model ? @child_model : (Class === @parent_model ? @parent_model.find_const(@child_model) : Object.find_const(@child_model))
end

# @api private
def get_children(parent, options = {}, finder = :all, *args)
bind_values = parent_values = parent_key.get(parent)
query_values = []
with_repository(parent) do |r|
query_values = r.identity_map(parent_model).keys.flatten
query_values.reject! { |k| r.identity_map(child_model)[[k]] }
end
bind_values = parent_key.get(parent)
parent_values = bind_values.dup

association_accessor = "#{self.name}_association"
with_repository(child_model) do |r|
parent_identity_map = parent.repository.identity_map(parent_model)
child_identity_map = r.identity_map(child_model)

query = {}
child_key.each do |key|
query[key] = query_values.empty? ? bind_values : query_values
end
query_values = parent_identity_map.keys.flatten
query_values.reject! { |k| child_identity_map[[k]] }

ret = nil
bind_values = query_values unless query_values.empty?
query = child_key.map { |k| [ k, bind_values ] }.to_hash

with_repository(parent) do
collection = child_model.send(finder, *(args.dup << @query.merge(options).merge(query)))
return collection unless collection.kind_of?(Collection) && collection.any?

Expand All @@ -90,11 +91,13 @@ def get_children(parent, options = {}, finder = :all, *args)
grouped_collection[get_parent(resource, parent)] << resource
end

association_accessor = "#{self.name}_association"

ret = nil
grouped_collection.each do |parent, children|
association = parent.send(association_accessor)

query = collection.query.dup

query.conditions.map! do |operator, property, bind_value|
if child_key.has_property?(property.name)
bind_value = *children.map { |child| property.get(child) }.uniq
Expand All @@ -112,45 +115,54 @@ def get_children(parent, options = {}, finder = :all, *args)
association.instance_variable_set(:@children, parents_children)
end
end
end

ret || child_model.send(finder, *(args.dup << @query.merge(options).merge(child_key.zip(bind_values).to_hash)))
ret || child_model.send(finder, *(args.dup << @query.merge(options).merge(child_key.zip(parent_values).to_hash)))
end
end

# @api private
def get_parent(child, parent = nil)
ret = nil
with_repository(parent || child) do |r|
bind_values = child_value = child_key.get(child)
return nil if child_value.any? { |bind_value| bind_value.nil? }
if parent = r.identity_map(parent_model)[child_value]
bind_values = child_key.get(child)
child_value = bind_values.dup
return nil unless child_value.nitems == child_value.size

with_repository(parent || parent_model) do
parent_identity_map = (parent || parent_model).repository.identity_map(parent_model)
child_identity_map = child.repository.identity_map(child_model)

if parent = parent_identity_map[child_value]
return parent
else
association_accessor = "#{self.name}_association"
children = r.identity_map(child_model)
children.each do |key, c|
bind_values |= child_key.get(c)
end
query_values = bind_values.reject { |k| r.identity_map(parent_model)[[k]] }
end

query = {}
parent_key.each do |key|
query[key] = query_values.empty? ? bind_values : query_values
end
children = child_identity_map.values

collection = parent_model.send(:all, query)
collection.send(:lazy_load)
children.each do |id, c|
c.send(association_accessor).instance_variable_set(:@parent, collection.get(*child_key.get(c)))
end
ret = child.send(association_accessor).instance_variable_get(:@parent)
bind_values |= children.map { |c| child_key.get(c) }
query_values = bind_values.reject { |k| parent_identity_map[[k]] }

bind_values = query_values unless query_values.empty?
query = parent_key.map { |k| [ k, bind_values ] }.to_hash

association_accessor = "#{self.name}_association"

collection = parent_model.send(:all, query)
collection.send(:lazy_load)
children.each do |c|
c.send(association_accessor).instance_variable_set(:@parent, collection.get(*child_key.get(c)))
end
child.send(association_accessor).instance_variable_get(:@parent)
end
ret
end

def with_repository(instance = nil, &block)
instance == nil ? yield(@repository) : yield(instance.repository)
# @api private
def with_repository(object = nil, &block)
other_model = object.model == child_model ? parent_model : child_model if object.respond_to?(:model)
other_model = object == child_model ? parent_model : child_model if object.kind_of?(DataMapper::Resource)

if other_model && other_model.repository == object.repository && object.repository.name != @repository_name
object.repository.scope(&block)
else
repository(@repository_name, &block)
end
end

# @api private
Expand All @@ -164,11 +176,11 @@ def attach_parent(child, parent)
# and parent_properties refer to the PK. For more information:
# http://edocs.bea.com/kodo/docs41/full/html/jdo_overview_mapping_join.html
# I wash my hands of it!
def initialize(name, repository, child_model, parent_model, options = {})
assert_kind_of 'name', name, Symbol
# assert_kind_of 'repository_name', repository_name, Symbol
assert_kind_of 'child_model', child_model, String, Class
assert_kind_of 'parent_model', parent_model, String, Class
def initialize(name, repository_name, child_model, parent_model, options = {})
assert_kind_of 'name', name, Symbol
assert_kind_of 'repository_name', repository_name, Symbol
assert_kind_of 'child_model', child_model, String, Class
assert_kind_of 'parent_model', parent_model, String, Class

if child_properties = options[:child_key]
assert_kind_of 'options[:child_key]', child_properties, Array
Expand All @@ -179,7 +191,7 @@ def initialize(name, repository, child_model, parent_model, options = {})
end

@name = name
@repository = repository
@repository_name = repository_name
@child_model = child_model
@child_properties = child_properties # may be nil
@query = options.reject { |k,v| OPTIONS.include?(k) }
Expand All @@ -193,6 +205,7 @@ def initialize(name, repository, child_model, parent_model, options = {})
end
end

# @api private
def model_defined?(model)
# TODO: figure out other ways to see if the model is loaded
model.kind_of?(Class)
Expand Down
48 changes: 27 additions & 21 deletions lib/dm-core/associations/relationship_chain.rb
Expand Up @@ -2,14 +2,15 @@ module DataMapper
module Associations
class RelationshipChain < Relationship
OPTIONS = [
:repository, :near_relationship_name, :remote_relationship_name,
:repository_name, :near_relationship_name, :remote_relationship_name,
:child_model, :parent_model, :parent_key, :child_key,
:min, :max
]

undef_method :get_parent
undef_method :attach_parent

# @api private
def child_model
near_relationship.child_model
end
Expand All @@ -29,10 +30,33 @@ def get_children(parent, options = {}, finder = :all, *args)

private

# @api private
def initialize(options)
if (missing_options = OPTIONS - [ :min, :max ] - options.keys ).any?
raise ArgumentError, "The options #{missing_options * ', '} are required", caller
end

@repository_name = options.fetch(:repository_name)
@near_relationship_name = options.fetch(:near_relationship_name)
@remote_relationship_name = options.fetch(:remote_relationship_name)
@child_model = options.fetch(:child_model)
@parent_model = options.fetch(:parent_model)
@parent_properties = options.fetch(:parent_key)
@child_properties = options.fetch(:child_key)
@mutable = options.delete(:mutable) || false

@name = near_relationship.name
@query = options.reject{ |key,val| OPTIONS.include?(key) }
@extra_links = []
@options = options
end

# @api private
def near_relationship
parent_model.relationships[@near_relationship_name]
end

# @api private
def links
if remote_relationship.kind_of?(RelationshipChain)
remote_relationship.instance_eval { links } + [remote_relationship.instance_eval { near_relationship } ]
Expand All @@ -41,34 +65,16 @@ def links
end
end

# @api private
def remote_relationship
near_relationship.child_model.relationships[@remote_relationship_name] ||
near_relationship.child_model.relationships[@remote_relationship_name.to_s.singularize.to_sym]
end

# @api private
def grandchild_model
Class === @child_model ? @child_model : (Class === @parent_model ? @parent_model.find_const(@child_model) : Object.find_const(@child_model))
end

def initialize(options)
if (missing_options = OPTIONS - [ :min, :max ] - options.keys ).any?
raise ArgumentError, "The options #{missing_options * ', '} are required", caller
end

@repository = options.fetch(:repository)
@near_relationship_name = options.fetch(:near_relationship_name)
@remote_relationship_name = options.fetch(:remote_relationship_name)
@child_model = options.fetch(:child_model)
@parent_model = options.fetch(:parent_model)
@parent_properties = options.fetch(:parent_key)
@child_properties = options.fetch(:child_key)
@mutable = options.delete(:mutable) || false

@name = near_relationship.name
@query = options.reject{ |key,val| OPTIONS.include?(key) }
@extra_links = []
@options = options
end
end # class Relationship
end # module Associations
end # module DataMapper
2 changes: 0 additions & 2 deletions lib/dm-core/auto_migrations.rb
Expand Up @@ -37,7 +37,6 @@ def auto_migrate!(repository_name = nil)
if self.superclass != Object
self.superclass.auto_migrate!(repository_name)
else
repository_name ||= default_repository_name
repository(repository_name) do |r|
r.adapter.destroy_model_storage(r, self)
r.adapter.create_model_storage(r, self)
Expand All @@ -51,7 +50,6 @@ def auto_migrate!(repository_name = nil)
#
# @param Symbol repository_name the repository to be migrated
def auto_upgrade!(repository_name = nil)
repository_name ||= default_repository_name
repository(repository_name) do |r|
r.adapter.upgrade_model_storage(r, self)
end
Expand Down

0 comments on commit af5c773

Please sign in to comment.