module DataMapper module Associations module ManyToOne #:nodoc: # Relationship class with implementation specific # to n side of 1 to n association class Relationship < Associations::Relationship OPTIONS = superclass::OPTIONS.dup << :required << :key # @api semipublic alias_method :source_repository_name, :child_repository_name # @api semipublic alias_method :source_model, :child_model # @api semipublic alias_method :target_repository_name, :parent_repository_name # @api semipublic alias_method :target_model, :parent_model # @api semipublic alias_method :target_key, :parent_key # @api semipublic def required? @required end # @api semipublic def key? @key end # @api private def nullable? klass = self.class warn "#{klass}#nullable? is deprecated, use #{klass}#required? instead (#{caller[0]})" !required? end # Returns a set of keys that identify child model # # @return [DataMapper::PropertySet] a set of properties that identify child model # @api private def child_key return @child_key if defined?(@child_key) model = child_model repository_name = child_repository_name || parent_repository_name properties = model.properties(repository_name) child_key = parent_key.zip(@child_properties || []).map do |parent_property, property_name| property_name ||= "#{name}_#{parent_property.name}".to_sym properties[property_name] || begin # create the property within the correct repository DataMapper.repository(repository_name) do model.property(property_name, parent_property.to_child_key, child_key_options(parent_property)) end end end @child_key = properties.class.new(child_key).freeze end # @api semipublic alias_method :source_key, :child_key # Returns a hash of conditions that scopes query that fetches # target object # # @return [Hash] # Hash of conditions that scopes query # # @api private def source_scope(source) if source.kind_of?(Resource) Query.target_conditions(source, source_key, target_key) else super end end # Returns a Resource for this relationship with a given source # # @param [Resource] source # A Resource to scope the collection with # @param [Query] other_query (optional) # A Query to further scope the collection with # # @return [Resource] # The resource scoped to the relationship, source and query # # @api private def resource_for(source, other_query = nil) query = query_for(source, other_query) # TODO: lookup the resource in the Identity Map, and make sure # it matches the query criteria, otherwise perform the query target_model.first(query) end # Loads and returns association target (ex.: author) for given source resource # (ex.: article) # # @param source [DataMapper::Resource] # Child object (ex.: instance of article) # @param other_query [DataMapper::Query] # Query options # # @api semipublic def get(source, query = nil) lazy_load(source) collection = get_collection(source) collection.first(query) if collection end def get_collection(source) resource = get!(source) resource.collection_for_self if resource end # Sets value of association target (ex.: author) for given source resource # (ex.: article) # # @param source [DataMapper::Resource] # Child object (ex.: instance of article) # # @param source [DataMapper::Resource] # Parent object (ex.: instance of author) # # @api semipublic def set(source, target) target = typecast(target) source_key.set(source, target_key.get(target)) set!(source, target) end # @api semipublic def default_for(source) typecast(super) end # Loads association target and sets resulting value on # given source resource # # @param [Resource] source # the source resource for the association # # @return [undefined] # # @api private def lazy_load(source) return if loaded?(source) || !valid_source?(source) # SEL: load all related resources in the source collection collection = source.collection if source.saved? && collection.size > 1 eager_load(collection) end unless loaded?(source) set!(source, resource_for(source)) end end private # Initializes the relationship, always using max cardinality of 1. # # @api semipublic def initialize(name, source_model, target_model, options = {}) if options.key?(:nullable) nullable_options = options.only(:nullable) required_options = { :required => !options.delete(:nullable) } warn "#{nullable_options.inspect} is deprecated, use #{required_options.inspect} instead (#{caller[2]})" options.update(required_options) end @required = options.fetch(:required, true) @key = options.fetch(:key, false) target_model ||= DataMapper::Inflector.camelize(name) options = { :min => @required ? 1 : 0, :max => 1 }.update(options) super end # Sets the association targets in the resource # # @param [Resource] source # the source to set # @param [Array(Resource)] targets # the target resource for the association # @param [Query, Hash] query # not used # # @return [undefined] # # @api private def eager_load_targets(source, targets, query) set(source, targets.first) end # @api private def typecast(target) if target.kind_of?(Hash) target_model.new(target) else target end end # Returns the inverse relationship class # # @api private def inverse_class OneToMany::Relationship end # Returns the inverse relationship name # # @api private def inverse_name super || DataMapper::Inflector.underscore(DataMapper::Inflector.demodulize(source_model.name)).pluralize.to_sym end # @api private def child_key_options(parent_property) options = parent_property.options.only(:length, :precision, :scale).update( :index => name, :required => required?, :key => key? ) if parent_property.primitive == Integer min = parent_property.min max = parent_property.max options.update(:min => min, :max => max) if min && max end options end # @api private def child_properties child_key.map { |property| property.name } end end # class Relationship end # module ManyToOne end # module Associations end # module DataMapper