module DataMapper::Model::Relationship
Public Class Methods
Initializes relationships hash for extended model class.
When model calls has n, has 1 or #belongs_to, relationships are stored in that hash: keys are repository names and values are relationship sets.
@api private
# File lib/dm-core/model/relationship.rb, line 19 def self.extended(model) model.instance_variable_set(:@relationships, {}) end
Public Instance Methods
A shorthand, clear syntax for defining many-to-one resource relationships.
* belongs_to :user # many to one user * belongs_to :friend, :model => 'User' # many to one friend * belongs_to :reference, :repository => :pubmed # association for repository other than default
@param name [Symbol]
the name that the association will be referenced by
@param *args [Model, Hash] model and/or options hash
@option *args :model[Model, String] The name of the class to associate with, if omitted
then the association name is assumed to match the class name
@option *args :repository name of child model repository
@return [Association::Relationship] The association created
should not be accessed directly
@api public
# File lib/dm-core/model/relationship.rb, line 156 def belongs_to(name, *args) name = name.to_sym model_name = self.name model = extract_model(args) options = extract_options(args) if options.key?(:through) raise "#{model_name}#belongs_to with :through is deprecated, use 'has 1, :#{name}, #{options.inspect}' in #{model_name} instead (#{caller.first})" elsif options.key?(:model) && model raise ArgumentError, 'should not specify options[:model] if passing the model in the third argument' end assert_valid_options(options) model ||= options.delete(:model) repository_name = repository.name # TODO: change to source_repository_name and target_respository_name options[:child_repository_name] = repository_name options[:parent_repository_name] = options.delete(:repository) relationship = Associations::ManyToOne::Relationship.new(name, self, model, options) relationships(repository_name) << relationship descendants.each do |descendant| descendant.relationships(repository_name) << relationship end create_relationship_reader(relationship) create_relationship_writer(relationship) relationship end
A shorthand, clear syntax for defining one-to-one, one-to-many and many-to-many resource relationships.
* has 1, :friend # one friend * has n, :friends # many friends * has 1..3, :friends # many friends (at least 1, at most 3) * has 3, :friends # many friends (exactly 3) * has 1, :friend, 'User' # one friend with the class User * has 3, :friends, :through => :friendships # many friends through the friendships relationship
@param cardinality [Integer, Range, Infinity]
cardinality that defines the association type and constraints
@param name [Symbol]
the name that the association will be referenced by
@param *args [Model, Hash] model and/or options hash
@option *args :through A association that this join should go through to form
a many-to-many association
@option *args :model[Model, String] The name of the class to associate with, if omitted
then the association name is assumed to match the class name
@option *args :repository name of child model repository
@return [Association::Relationship] the relationship that was
created to reflect either a one-to-one, one-to-many or many-to-many relationship
@raise [ArgumentError] if the cardinality was not understood. Should be a
Integer, Range or Infinity(n)
@api public
# File lib/dm-core/model/relationship.rb, line 96 def has(cardinality, name, *args) name = name.to_sym model = extract_model(args) options = extract_options(args) min, max = extract_min_max(cardinality) options.update(:min => min, :max => max) assert_valid_options(options) if options.key?(:model) && model raise ArgumentError, 'should not specify options[:model] if passing the model in the third argument' end model ||= options.delete(:model) repository_name = repository.name # TODO: change to :target_respository_name and :source_repository_name options[:child_repository_name] = options.delete(:repository) options[:parent_repository_name] = repository_name klass = if max > 1 options.key?(:through) ? Associations::ManyToMany::Relationship : Associations::OneToMany::Relationship else Associations::OneToOne::Relationship end relationship = klass.new(name, model, self, options) relationships(repository_name) << relationship descendants.each do |descendant| descendant.relationships(repository_name) << relationship end create_relationship_reader(relationship) create_relationship_writer(relationship) relationship end
When DataMapper model is inherited, relationships of parent are duplicated and copied to subclass model
@api private
# File lib/dm-core/model/relationship.rb, line 27 def inherited(model) model.instance_variable_set(:@relationships, {}) @relationships.each do |repository_name, relationships| model_relationships = model.relationships(repository_name) relationships.each { |relationship| model_relationships << relationship } end super end
Used to express unlimited cardinality of association, see has
@api public
# File lib/dm-core/model/relationship.rb, line 63 def n Infinity end
Returns copy of relationships set in given repository.
@param [Symbol] repository_name
Name of the repository for which relationships set is returned
@return [RelationshipSet] relationships set for given repository
@api semipublic
# File lib/dm-core/model/relationship.rb, line 45 def relationships(repository_name = default_repository_name) # TODO: create RelationshipSet#copy that will copy the relationships, but assign the # new Relationship objects to a supplied repository and model. dup does not really # do what is needed default_repository_name = self.default_repository_name @relationships[repository_name] ||= if repository_name == default_repository_name RelationshipSet.new else relationships(default_repository_name).dup end end
Private Instance Methods
Validates options of association method like #belongs_to or has: verifies types of cardinality bounds, repository, association class, keys and possible values of :through option.
@api private
# File lib/dm-core/model/relationship.rb, line 250 def assert_valid_options(options) # TODO: update to match Query#assert_valid_options # - perform options normalization elsewhere if options.key?(:min) && options.key?(:max) min = options[:min] max = options[:max] min = min.to_int unless min == Infinity max = max.to_int unless max == Infinity if min == Infinity && max == Infinity raise ArgumentError, 'Cardinality may not be n..n. The cardinality specifies the min/max number of results from the association' elsif min > max raise ArgumentError, "Cardinality min (#{min}) cannot be larger than the max (#{max})" elsif min < 0 raise ArgumentError, "Cardinality min much be greater than or equal to 0, but was #{min}" elsif max < 1 raise ArgumentError, "Cardinality max much be greater than or equal to 1, but was #{max}" end end if options.key?(:repository) options[:repository] = options[:repository].to_sym end if options.key?(:class_name) raise "+options[:class_name]+ is deprecated, use :model instead (#{caller[1]})" elsif options.key?(:remote_name) raise "+options[:remote_name]+ is deprecated, use :via instead (#{caller[1]})" end if options.key?(:through) assert_kind_of 'options[:through]', options[:through], Symbol, Module end [ :via, :inverse ].each do |key| if options.key?(key) assert_kind_of "options[#{key.inspect}]", options[key], Symbol, Associations::Relationship end end # TODO: deprecate :child_key and :parent_key in favor of :source_key and # :target_key (will mean something different for each relationship) [ :child_key, :parent_key ].each do |key| if options.key?(key) options[key] = Array(options[key]) end end if options.key?(:limit) raise ArgumentError, '+options[:limit]+ should not be specified on a relationship' end end
Dynamically defines reader method
@api private
# File lib/dm-core/model/relationship.rb, line 323 def create_relationship_reader(relationship) name = relationship.name reader_name = name.to_s return if method_defined?(reader_name) reader_visibility = relationship.reader_visibility relationship_module.module_eval " #{reader_visibility} def #{reader_name}(query = nil) # TODO: when no query is passed in, return the results from # the ivar directly. This will require that the ivar # actually hold the resource/collection, and in the case # of 1:1, the underlying collection is hidden in a # private ivar, and the resource is in a known ivar persistence_state.get(relationships[#{name.inspect}], query) end ", __FILE__, __LINE__ + 1 end
Dynamically defines writer method
@api private
# File lib/dm-core/model/relationship.rb, line 348 def create_relationship_writer(relationship) name = relationship.name writer_name = "#{name}=" return if method_defined?(writer_name) writer_visibility = relationship.writer_visibility relationship_module.module_eval " #{writer_visibility} def #{writer_name}(target) relationship = relationships[#{name.inspect}] self.persistence_state = persistence_state.set(relationship, target) persistence_state.get(relationship) end ", __FILE__, __LINE__ + 1 end
A support method for converting Integer, Range or Infinity values into two values representing the minimum and maximum cardinality of the association
@return [Array] A pair of integers, min and max
@api private
# File lib/dm-core/model/relationship.rb, line 235 def extract_min_max(cardinality) case cardinality when Integer then [ cardinality, cardinality ] when Range then [ cardinality.first, cardinality.last ] when Infinity then [ 0, Infinity ] else assert_kind_of 'options', options, Integer, Range, Infinity.class end end
Extract the model from an Array of arguments
@param [Array(Model, String, Hash)]
The arguments passed to an relationship declaration
@return [Model, to_str]
target model for the association
@api private
# File lib/dm-core/model/relationship.rb, line 203 def extract_model(args) model = args.first if model.kind_of?(Model) model elsif model.respond_to?(:to_str) model.to_str else nil end end
Extract the model from an Array of arguments
@param [Array(Model, String, Hash)]
The arguments passed to an relationship declaration
@return [Hash]
options for the association
@api private
# File lib/dm-core/model/relationship.rb, line 224 def extract_options(args) options = args.last options.respond_to?(:to_hash) ? options.to_hash.dup : {} end
@api public
# File lib/dm-core/model/relationship.rb, line 367 def method_missing(method, *args, &block) if relationship = relationships(repository_name)[method] return Query::Path.new([ relationship ]) end super end
Defines the anonymous module that is used to add relationships. Using a single module here prevents having a very large number of anonymous modules, where each property has their own module. @api private
# File lib/dm-core/model/relationship.rb, line 310 def relationship_module @relationship_module ||= begin mod = Module.new class_eval do include mod end mod end end