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.
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
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
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
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
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
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 446: def initialize(name, child_model, parent_model, options = {}) 447: initialize_object_ivar('child_model', child_model) 448: initialize_object_ivar('parent_model', parent_model) 449: 450: @name = name 451: @instance_variable_name = "@#{@name}".freeze 452: @options = options.dup.freeze 453: @child_repository_name = @options[:child_repository_name] 454: @parent_repository_name = @options[:parent_repository_name] 455: 456: unless @options[:child_key].nil? 457: @child_properties = DataMapper::Ext.try_dup(@options[:child_key]).freeze 458: end 459: unless @options[:parent_key].nil? 460: @parent_properties = DataMapper::Ext.try_dup(@options[:parent_key]).freeze 461: end 462: 463: @min = @options[:min] 464: @max = @options[:max] 465: @reader_visibility = @options.fetch(:reader_visibility, :public) 466: @writer_visibility = @options.fetch(:writer_visibility, :public) 467: @default = @options.fetch(:default, nil) 468: 469: # TODO: normalize the @query to become :conditions => AndOperation 470: # - Property/Relationship/Path should be left alone 471: # - Symbol/String keys should become a Property, scoped to the target_repository and target_model 472: # - Extract subject (target) from Operator 473: # - subject should be processed same as above 474: # - each subject should be transformed into AbstractComparison 475: # object with the subject, operator and value 476: # - transform into an AndOperation object, and return the 477: # query as :condition => and_object from self.query 478: # - this should provide the best performance 479: 480: @query = DataMapper::Ext::Hash.except(@options, *self.class::OPTIONS).freeze 481: end
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 368: def ==(other) 369: return true if equal?(other) 370: other.respond_to?(:cmp_repository?, true) && 371: other.respond_to?(:cmp_model?, true) && 372: other.respond_to?(:cmp_key?, true) && 373: other.respond_to?(:min) && 374: other.respond_to?(:max) && 375: other.respond_to?(:query) && 376: cmp?(other, :==) 377: 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 195: def child_key 196: return @child_key if defined?(@child_key) 197: 198: repository_name = child_repository_name || parent_repository_name 199: properties = child_model.properties(repository_name) 200: 201: @child_key = if @child_properties 202: child_key = properties.values_at(*@child_properties) 203: properties.class.new(child_key).freeze 204: else 205: properties.key 206: end 207: 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 168: def child_model 169: return @child_model if defined?(@child_model) 170: child_model_name = self.child_model_name 171: @child_model = DataMapper::Ext::Module.find_const(@parent_model || Object, child_model_name) 172: rescue NameError 173: raise NameError, "Cannot find the child_model #{child_model_name} for #{parent_model_name} in #{name}" 174: end
@api private
# File lib/dm-core/associations/relationship.rb, line 177 177: def child_model? 178: child_model 179: true 180: rescue NameError 181: false 182: end
@api private
# File lib/dm-core/associations/relationship.rb, line 185 185: def child_model_name 186: @child_model ? child_model.name : @child_model_name 187: 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 307: def eager_load(source, query = nil) 308: targets = source.model.all(query_for(source, query)) 309: 310: # FIXME: cannot associate targets to m:m collection yet 311: if source.loaded? && !source.kind_of?(ManyToMany::Collection) 312: associate_targets(source, targets) 313: end 314: 315: targets 316: 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 354: def eql?(other) 355: return true if equal?(other) 356: instance_of?(other.class) && cmp?(other, :eql?) 357: 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 131: def field 132: name.to_s 133: 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 266: def get(resource, other_query = nil) 267: raise NotImplementedError, "#{self.class}#get not implemented" 268: 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 275: def get!(resource) 276: resource.instance_variable_get(instance_variable_name) 277: end
@api private
# File lib/dm-core/associations/relationship.rb, line 416 416: def hash 417: self.class.hash ^ 418: name.hash ^ 419: child_repository_name.hash ^ 420: parent_repository_name.hash ^ 421: child_model.hash ^ 422: parent_model.hash ^ 423: child_properties.hash ^ 424: parent_properties.hash ^ 425: min.hash ^ 426: max.hash ^ 427: query.hash 428: end
Get the inverse relationship from the target model
@api semipublic
# File lib/dm-core/associations/relationship.rb, line 382 382: def inverse 383: return @inverse if defined?(@inverse) 384: 385: @inverse = options[:inverse] 386: 387: if kind_of_inverse?(@inverse) 388: return @inverse 389: end 390: 391: relationships = target_model.relationships(relative_target_repository_name) 392: 393: @inverse = relationships.detect { |relationship| inverse?(relationship) } || 394: invert 395: 396: @inverse.child_key 397: 398: @inverse 399: end
Checks if “other end” of association is loaded on given resource.
@api semipublic
# File lib/dm-core/associations/relationship.rb, line 322 322: def loaded?(resource) 323: resource.instance_variable_defined?(instance_variable_name) 324: 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 248: def parent_key 249: return @parent_key if defined?(@parent_key) 250: 251: repository_name = parent_repository_name || child_repository_name 252: properties = parent_model.properties(repository_name) 253: 254: @parent_key = if @parent_properties 255: parent_key = properties.values_at(*@parent_properties) 256: properties.class.new(parent_key).freeze 257: else 258: properties.key 259: end 260: 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 221: def parent_model 222: return @parent_model if defined?(@parent_model) 223: parent_model_name = self.parent_model_name 224: @parent_model = DataMapper::Ext::Module.find_const(@child_model || Object, parent_model_name) 225: rescue NameError 226: raise NameError, "Cannot find the parent_model #{parent_model_name} for #{child_model_name} in #{name}" 227: end
@api private
# File lib/dm-core/associations/relationship.rb, line 230 230: def parent_model? 231: parent_model 232: true 233: rescue NameError 234: false 235: end
@api private
# File lib/dm-core/associations/relationship.rb, line 238 238: def parent_model_name 239: @parent_model ? parent_model.name : @parent_model_name 240: 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 150: def query_for(source, other_query = nil) 151: repository_name = relative_target_repository_name_for(source) 152: 153: DataMapper.repository(repository_name).scope do 154: query = target_model.query.dup 155: query.update(self.query) 156: query.update(:conditions => source_scope(source)) 157: query.update(other_query) if other_query 158: query.update(:fields => query.fields | target_key) 159: end 160: end
@api private
# File lib/dm-core/associations/relationship.rb, line 402 402: def relative_target_repository_name 403: target_repository_name || source_repository_name 404: end
@api private
# File lib/dm-core/associations/relationship.rb, line 407 407: def relative_target_repository_name_for(source) 408: target_repository_name || if source.respond_to?(:repository) 409: source.repository.name 410: else 411: source_repository_name 412: end 413: 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 283: def set(resource, association) 284: raise NotImplementedError, "#{self.class}#set not implemented" 285: 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 292: def set!(resource, association) 293: resource.instance_variable_set(instance_variable_name, association) 294: 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 142: def source_scope(source) 143: { inverse => source } 144: 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 335: def valid?(value, negated = false) 336: case value 337: when Enumerable then valid_target_collection?(value, negated) 338: when Resource then valid_target?(value) 339: when nil then true 340: else 341: raise ArgumentError, "+value+ should be an Enumerable, Resource or nil, but was a #{value.class.name}" 342: end 343: end
# File lib/dm-core/associations/relationship.rb, line 645 645: def associate_targets(source, targets) 646: # TODO: create an object that wraps this logic, and when the first 647: # kicker is fired, then it'll load up the collection, and then 648: # populate all the other methods 649: 650: target_maps = Hash.new { |hash, key| hash[key] = [] } 651: 652: targets.each do |target| 653: target_maps[target_key.get(target)] << target 654: end 655: 656: Array(source).each do |source| 657: key = source_key.get(source) 658: eager_load_targets(source, target_maps[key], query) 659: end 660: end
@api private
# File lib/dm-core/associations/relationship.rb, line 605 605: def cmp?(other, operator) 606: name.send(operator, other.name) && 607: cmp_repository?(other, operator, :child) && 608: cmp_repository?(other, operator, :parent) && 609: cmp_model?(other, operator, :child) && 610: cmp_model?(other, operator, :parent) && 611: cmp_key?(other, operator, :child) && 612: cmp_key?(other, operator, :parent) && 613: min.send(operator, other.min) && 614: max.send(operator, other.max) && 615: query.send(operator, other.query) 616: end
@api private
# File lib/dm-core/associations/relationship.rb, line 636 636: def cmp_key?(other, operator, type) 637: property_method = "#{type}_properties" 638: 639: self_key = send(property_method) 640: other_key = other.send(property_method) 641: 642: self_key.send(operator, other_key) 643: end
@api private
# File lib/dm-core/associations/relationship.rb, line 629 629: def cmp_model?(other, operator, type) 630: send("#{type}_model?") && 631: other.send("#{type}_model?") && 632: send("#{type}_model").base_model.send(operator, other.send("#{type}_model").base_model) 633: end
@api private
# File lib/dm-core/associations/relationship.rb, line 619 619: def cmp_repository?(other, operator, type) 620: # if either repository is nil, then the relationship is relative, 621: # and the repositories are considered equivalent 622: return true unless repository_name = send("#{type}_repository_name") 623: return true unless other_repository_name = other.send("#{type}_repository_name") 624: 625: repository_name.send(operator, other_repository_name) 626: end
Sets the association targets in the resource
@param [Resource] source
the source to set
@param [Array
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 528: def eager_load_targets(source, targets, query) 529: raise NotImplementedError, "#{self.class}#eager_load_targets not implemented" 530: 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 [#, #, #] 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 501: def initialize_object_ivar(name, object) 502: if object.respond_to?(:name) 503: instance_variable_set("@#{name}", object) 504: initialize_object_ivar(name, object.name) 505: elsif object.respond_to?(:to_str) 506: instance_variable_set("@#{name}_name", object.to_str.dup.freeze) 507: elsif object.respond_to?(:to_sym) 508: instance_variable_set("@#{name}_name", object.to_sym) 509: else 510: raise ArgumentError, "#{name} does not respond to #to_str or #name" 511: end 512: 513: object 514: end
@api private
# File lib/dm-core/associations/relationship.rb, line 563 563: def inverse?(other) 564: return true if @inverse.equal?(other) 565: 566: other != self && 567: kind_of_inverse?(other) && 568: cmp_repository?(other, :==, :child) && 569: cmp_repository?(other, :==, :parent) && 570: cmp_model?(other, :==, :child) && 571: cmp_model?(other, :==, :parent) && 572: cmp_key?(other, :==, :child) && 573: cmp_key?(other, :==, :parent) 574: 575: # TODO: match only when the Query is empty, or is the same as the 576: # default scope for the target model 577: end
@api private
# File lib/dm-core/associations/relationship.rb, line 580 580: def inverse_name 581: inverse = options[:inverse] 582: if inverse.kind_of?(Relationship) 583: inverse.name 584: else 585: inverse 586: end 587: end
@api private
# File lib/dm-core/associations/relationship.rb, line 590 590: def invert 591: inverse_class.new(inverse_name, child_model, parent_model, inverted_options) 592: end
@api private
# File lib/dm-core/associations/relationship.rb, line 595 595: def inverted_options 596: DataMapper::Ext::Hash.only(options, *OPTIONS - [ :min, :max ]).update(:inverse => self) 597: end
@api private
# File lib/dm-core/associations/relationship.rb, line 600 600: def kind_of_inverse?(other) 601: other.kind_of?(inverse_class) 602: end
Access Relationship#child_key directly
@api private
@api private
# File lib/dm-core/associations/relationship.rb, line 557 557: def valid_source?(source) 558: source.kind_of?(source_model) && 559: target_key.valid?(source_key.get(source)) 560: end
@api private
# File lib/dm-core/associations/relationship.rb, line 551 551: def valid_target?(target) 552: target.kind_of?(target_model) && 553: source_key.valid?(target_key.get(target)) 554: end
@api private
# File lib/dm-core/associations/relationship.rb, line 533 533: def valid_target_collection?(collection, negated) 534: if collection.kind_of?(Collection) 535: # TODO: move the check for model_key into Collection#reloadable? 536: # since what we're really checking is a Collection's ability 537: # to reload itself, which is (currently) only possible if the 538: # key was loaded. 539: model = target_model 540: model_key = model.key(repository.name) 541: 542: collection.model <= model && 543: (collection.query.fields & model_key) == model_key && 544: (collection.loaded? ? (collection.any? || negated) : true) 545: else 546: collection.all? { |resource| valid_target?(resource) } 547: end 548: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.