class DataMapper::Associations::Relationship
Base class for relationships. Each type of relationship (1 to 1, 1 to n, n to m) implements a subclass of this class with methods like get and set overridden.
Constants
- OPTIONS
Attributes
@api private
Repository from where child objects are loaded
@api semipublic
ivar used to store collection of child options in source
@example for :commits association in
class VersionControl::Branch # ... has n, :commits end
instance variable name for source will be @commits
@api semipublic
Maximum number of child objects for relationship
@example for :fouls association in
class Basketball::Player # ... has 0..5, :fouls end
maximum is 5
@api semipublic
Minimum number of child objects for relationship
@example for :cores association in
class CPU::Multicore # ... has 2..n, :cores end
minimum is 2
@api semipublic
Relationship name
@example for :parent association in
class VersionControl::Commit # ... belongs_to :parent end
name is :parent
@api semipublic
Options used to set up association of this relationship
@example for :author association in
class VersionControl::Commit # ... belongs_to :author, :model => 'Person' end
options is a hash with a single key, :model
@api semipublic
@api private
Repository from where parent objects are loaded
@api semipublic
Returns query options for relationship.
For this base class, always returns query options has been initialized with. Overriden in subclasses.
@api private
Returns the visibility for the source accessor
@return [Symbol]
the visibility for the accessor added to the source
@api semipublic
Returns the visibility for the source mutator
@return [Symbol]
the visibility for the mutator added to the source
@api semipublic
Public Class Methods
Initializes new Relationship: sets attributes of relationship from options as well as conventions: for instance, @ivar name for association is constructed by prefixing @ to association name.
Once attributes are set, reader and writer are created for the resource association belongs to
@api semipublic
# File lib/dm-core/associations/relationship.rb, line 446 def initialize(name, child_model, parent_model, options = {}) initialize_object_ivar('child_model', child_model) initialize_object_ivar('parent_model', parent_model) @name = name @instance_variable_name = "@#{@name}".freeze @options = options.dup.freeze @child_repository_name = @options[:child_repository_name] @parent_repository_name = @options[:parent_repository_name] unless @options[:child_key].nil? @child_properties = DataMapper::Ext.try_dup(@options[:child_key]).freeze end unless @options[:parent_key].nil? @parent_properties = DataMapper::Ext.try_dup(@options[:parent_key]).freeze end @min = @options[:min] @max = @options[:max] @reader_visibility = @options.fetch(:reader_visibility, :public) @writer_visibility = @options.fetch(:writer_visibility, :public) @default = @options.fetch(:default, nil) # TODO: normalize the @query to become :conditions => AndOperation # - Property/Relationship/Path should be left alone # - Symbol/String keys should become a Property, scoped to the target_repository and target_model # - Extract subject (target) from Operator # - subject should be processed same as above # - each subject should be transformed into AbstractComparison # object with the subject, operator and value # - transform into an AndOperation object, and return the # query as :condition => and_object from self.query # - this should provide the best performance @query = DataMapper::Ext::Hash.except(@options, *self.class::OPTIONS).freeze end
Public Instance Methods
Compares another Relationship for equivalency
@param [Relationship] other
the other Relationship to compare with
@return [Boolean]
true if they are equal, false if not
@api public
# File lib/dm-core/associations/relationship.rb, line 368 def ==(other) return true if equal?(other) other.respond_to?(:cmp_repository?, true) && other.respond_to?(:cmp_model?, true) && other.respond_to?(:cmp_key?, true) && other.respond_to?(:min) && other.respond_to?(:max) && other.respond_to?(:query) && cmp?(other, :==) end
Returns a set of keys that identify the target model
@return [PropertySet]
a set of properties that identify the target model
@api semipublic
# File lib/dm-core/associations/relationship.rb, line 195 def child_key return @child_key if defined?(@child_key) repository_name = child_repository_name || parent_repository_name properties = child_model.properties(repository_name) @child_key = if @child_properties child_key = properties.values_at(*@child_properties) properties.class.new(child_key).freeze else properties.key end end
Returns model class used by child side of the relationship
@return [Resource]
Model for association child
@api private
# File lib/dm-core/associations/relationship.rb, line 168 def child_model return @child_model if defined?(@child_model) child_model_name = self.child_model_name @child_model = DataMapper::Ext::Module.find_const(@parent_model || Object, child_model_name) rescue NameError raise NameError, "Cannot find the child_model #{child_model_name} for #{parent_model_name} in #{name}" end
@api private
# File lib/dm-core/associations/relationship.rb, line 177 def child_model? child_model true rescue NameError false end
@api private
# File lib/dm-core/associations/relationship.rb, line 185 def child_model_name @child_model ? child_model.name : @child_model_name end
Eager load the collection using the source as a base
@param [Collection] source
the source collection to query with
@param [Query, Hash] query
optional query to restrict the collection
@return [Collection]
the loaded collection for the source
@api private
# File lib/dm-core/associations/relationship.rb, line 307 def eager_load(source, query = nil) targets = source.model.all(query_for(source, query)) # FIXME: cannot associate targets to m:m collection yet if source.loaded? && !source.kind_of?(ManyToMany::Collection) associate_targets(source, targets) end targets end
Compares another Relationship for equality
@param [Relationship] other
the other Relationship to compare with
@return [Boolean]
true if they are equal, false if not
@api public
# File lib/dm-core/associations/relationship.rb, line 354 def eql?(other) return true if equal?(other) instance_of?(other.class) && cmp?(other, :eql?) end
Returns the String the Relationship would use in a Hash
@return [String]
String name for the Relationship
@api private
# File lib/dm-core/associations/relationship.rb, line 131 def field name.to_s end
Loads and returns “other end” of the association. Must be implemented in subclasses.
@api semipublic
# File lib/dm-core/associations/relationship.rb, line 266 def get(resource, other_query = nil) raise NotImplementedError, "#{self.class}#get not implemented" end
Gets “other end” of the association directly as @ivar on given resource. Subclasses usually use implementation of this class.
@api semipublic
# File lib/dm-core/associations/relationship.rb, line 275 def get!(resource) resource.instance_variable_get(instance_variable_name) end
@api private
# File lib/dm-core/associations/relationship.rb, line 416 def hash self.class.hash ^ name.hash ^ child_repository_name.hash ^ parent_repository_name.hash ^ child_model.hash ^ parent_model.hash ^ child_properties.hash ^ parent_properties.hash ^ min.hash ^ max.hash ^ query.hash end
Get the inverse relationship from the target model
@api semipublic
# File lib/dm-core/associations/relationship.rb, line 382 def inverse return @inverse if defined?(@inverse) @inverse = options[:inverse] if kind_of_inverse?(@inverse) return @inverse end relationships = target_model.relationships(relative_target_repository_name) @inverse = relationships.detect { |relationship| inverse?(relationship) } || invert @inverse.child_key @inverse end
Checks if “other end” of association is loaded on given resource.
@api semipublic
# File lib/dm-core/associations/relationship.rb, line 322 def loaded?(resource) resource.instance_variable_defined?(instance_variable_name) end
Returns a set of keys that identify parent model
@return [PropertySet]
a set of properties that identify parent model
@api private
# File lib/dm-core/associations/relationship.rb, line 248 def parent_key return @parent_key if defined?(@parent_key) repository_name = parent_repository_name || child_repository_name properties = parent_model.properties(repository_name) @parent_key = if @parent_properties parent_key = properties.values_at(*@parent_properties) properties.class.new(parent_key).freeze else properties.key end end
Returns model class used by parent side of the relationship
@return [Resource]
Class of association parent
@api private
# File lib/dm-core/associations/relationship.rb, line 221 def parent_model return @parent_model if defined?(@parent_model) parent_model_name = self.parent_model_name @parent_model = DataMapper::Ext::Module.find_const(@child_model || Object, parent_model_name) rescue NameError raise NameError, "Cannot find the parent_model #{parent_model_name} for #{child_model_name} in #{name}" end
@api private
# File lib/dm-core/associations/relationship.rb, line 230 def parent_model? parent_model true rescue NameError false end
@api private
# File lib/dm-core/associations/relationship.rb, line 238 def parent_model_name @parent_model ? parent_model.name : @parent_model_name end
Creates and returns Query instance that fetches target resource(s) (ex.: articles) for given target resource (ex.: author)
@api semipublic
# File lib/dm-core/associations/relationship.rb, line 150 def query_for(source, other_query = nil) repository_name = relative_target_repository_name_for(source) DataMapper.repository(repository_name).scope do query = target_model.query.dup query.update(self.query) query.update(:conditions => source_scope(source)) query.update(other_query) if other_query query.update(:fields => query.fields | target_key) end end
@api private
# File lib/dm-core/associations/relationship.rb, line 402 def relative_target_repository_name target_repository_name || source_repository_name end
@api private
# File lib/dm-core/associations/relationship.rb, line 407 def relative_target_repository_name_for(source) target_repository_name || if source.respond_to?(:repository) source.repository.name else source_repository_name end end
Sets value of the “other end” of association on given resource. Must be implemented in subclasses.
@api semipublic
# File lib/dm-core/associations/relationship.rb, line 283 def set(resource, association) raise NotImplementedError, "#{self.class}#set not implemented" end
Sets “other end” of the association directly as @ivar on given resource. Subclasses usually use implementation of this class.
@api semipublic
# File lib/dm-core/associations/relationship.rb, line 292 def set!(resource, association) resource.instance_variable_set(instance_variable_name, association) end
Returns a hash of conditions that scopes query that fetches target object
@return [Hash]
Hash of conditions that scopes query
@api private
# File lib/dm-core/associations/relationship.rb, line 142 def source_scope(source) { inverse => source } end
Test the resource to see if it is a valid target
@param [Object] source
the resource or collection to be tested
@return [Boolean]
true if the resource is valid
@api semipulic
# File lib/dm-core/associations/relationship.rb, line 335 def valid?(value, negated = false) case value when Enumerable then valid_target_collection?(value, negated) when Resource then valid_target?(value) when nil then true else raise ArgumentError, "+value+ should be an Enumerable, Resource or nil, but was a #{value.class.name}" end end
Private Instance Methods
# File lib/dm-core/associations/relationship.rb, line 645 def associate_targets(source, targets) # TODO: create an object that wraps this logic, and when the first # kicker is fired, then it'll load up the collection, and then # populate all the other methods target_maps = Hash.new { |hash, key| hash[key] = [] } targets.each do |target| target_maps[target_key.get(target)] << target end Array(source).each do |source| key = source_key.get(source) eager_load_targets(source, target_maps[key], query) end end
@api private
# File lib/dm-core/associations/relationship.rb, line 605 def cmp?(other, operator) name.send(operator, other.name) && cmp_repository?(other, operator, :child) && cmp_repository?(other, operator, :parent) && cmp_model?(other, operator, :child) && cmp_model?(other, operator, :parent) && cmp_key?(other, operator, :child) && cmp_key?(other, operator, :parent) && min.send(operator, other.min) && max.send(operator, other.max) && query.send(operator, other.query) end
@api private
# File lib/dm-core/associations/relationship.rb, line 636 def cmp_key?(other, operator, type) property_method = "#{type}_properties" self_key = send(property_method) other_key = other.send(property_method) self_key.send(operator, other_key) end
@api private
# File lib/dm-core/associations/relationship.rb, line 629 def cmp_model?(other, operator, type) send("#{type}_model?") && other.send("#{type}_model?") && send("#{type}_model").base_model.send(operator, other.send("#{type}_model").base_model) end
@api private
# File lib/dm-core/associations/relationship.rb, line 619 def cmp_repository?(other, operator, type) # if either repository is nil, then the relationship is relative, # and the repositories are considered equivalent return true unless repository_name = send("#{type}_repository_name") return true unless other_repository_name = other.send("#{type}_repository_name") repository_name.send(operator, other_repository_name) end
Sets the association targets in the resource
@param [Resource] source
the source to set
@param [Array<Resource>] targets
the targets for the association
@param [Query, Hash] query
the query to scope the association with
@return [undefined]
@api private
# File lib/dm-core/associations/relationship.rb, line 528 def eager_load_targets(source, targets, query) raise NotImplementedError, "#{self.class}#eager_load_targets not implemented" end
Set the correct ivars for the named object
This method should set the object in an ivar with the same name provided, plus it should set a String form of the object in a second ivar.
@param [String]
the name of the ivar to set
@param [#name, to_str, to_sym] object
the object to set in the ivar
@return [String]
the String value
@raise [ArgumentError]
raise when object does not respond to expected methods
@api private
# File lib/dm-core/associations/relationship.rb, line 501 def initialize_object_ivar(name, object) if object.respond_to?(:name) instance_variable_set("@#{name}", object) initialize_object_ivar(name, object.name) elsif object.respond_to?(:to_str) instance_variable_set("@#{name}_name", object.to_str.dup.freeze) elsif object.respond_to?(:to_sym) instance_variable_set("@#{name}_name", object.to_sym) else raise ArgumentError, "#{name} does not respond to #to_str or #name" end object end
@api private
# File lib/dm-core/associations/relationship.rb, line 563 def inverse?(other) return true if @inverse.equal?(other) other != self && kind_of_inverse?(other) && cmp_repository?(other, :==, :child) && cmp_repository?(other, :==, :parent) && cmp_model?(other, :==, :child) && cmp_model?(other, :==, :parent) && cmp_key?(other, :==, :child) && cmp_key?(other, :==, :parent) # TODO: match only when the Query is empty, or is the same as the # default scope for the target model end
@api private
# File lib/dm-core/associations/relationship.rb, line 580 def inverse_name inverse = options[:inverse] if inverse.kind_of?(Relationship) inverse.name else inverse end end
@api private
# File lib/dm-core/associations/relationship.rb, line 590 def invert inverse_class.new(inverse_name, child_model, parent_model, inverted_options) end
@api private
# File lib/dm-core/associations/relationship.rb, line 595 def inverted_options DataMapper::Ext::Hash.only(options, *OPTIONS - [ :min, :max ]).update(:inverse => self) end
@api private
# File lib/dm-core/associations/relationship.rb, line 600 def kind_of_inverse?(other) other.kind_of?(inverse_class) end
@api private
# File lib/dm-core/associations/relationship.rb, line 557 def valid_source?(source) source.kind_of?(source_model) && target_key.valid?(source_key.get(source)) end
@api private
# File lib/dm-core/associations/relationship.rb, line 551 def valid_target?(target) target.kind_of?(target_model) && source_key.valid?(target_key.get(target)) end
@api private
# File lib/dm-core/associations/relationship.rb, line 533 def valid_target_collection?(collection, negated) if collection.kind_of?(Collection) # TODO: move the check for model_key into Collection#reloadable? # since what we're really checking is a Collection's ability # to reload itself, which is (currently) only possible if the # key was loaded. model = target_model model_key = model.key(repository.name) collection.model <= model && (collection.query.fields & model_key) == model_key && (collection.loaded? ? (collection.any? || negated) : true) else collection.all? { |resource| valid_target?(resource) } end end