lib/dm-core/associations/many_to_one.rb
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 << :unique
# @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 semipublic
def unique?
!!@unique
end
# @deprecated
def nullable?
raise "#{self.class}#nullable? is deprecated, use #{self.class}#required? instead (#{caller.first})"
end
# Returns a set of keys that identify source model
#
# @return [DataMapper::PropertySet] a set of properties that identify source model
# @api private
def child_key
return @child_key if defined?(@child_key)
model = source_model
repository_name = source_repository_name || target_repository_name
properties = model.properties(repository_name)
source_key = target_key.zip(@child_properties || []).map do |target_property, property_name|
property_name ||= "#{name}_#{target_property.name}".to_sym
properties[property_name] || begin
# create the property within the correct repository
DataMapper.repository(repository_name) do
model.property(property_name, target_property.to_child_key, source_key_options(target_property))
end
end
end
@child_key = properties.class.new(source_key).freeze
end
# @api semipublic
alias_method :source_key, :child_key
# Initialize the foreign key property this "many to one"
# relationship uses to persist itself
#
# @return [self]
#
# @api public
def finalize
child_key
self
end
# 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)
# If the target key is equal to the model key, we can use the
# Model#get so the IdentityMap is used
if target_key == target_model.key
target = target_model.get(*source_key.get!(source))
if query.conditions.matches?(target)
target
else
nil
end
else
target_model.first(query)
end
end
# Loads and returns association target (ex.: author) for given source resource
# (ex.: article)
#
# @param source [DataMapper::Resource]
# source object (ex.: instance of article)
# @param other_query [DataMapper::Query]
# Query options
#
# @api semipublic
def get(source, query = nil)
lazy_load(source)
if query
collection = get_collection(source)
collection.first(query) if collection
else
get!(source)
end
end
def get_collection(source)
target = get!(source)
target.collection_for_self if target
end
# Sets value of association target (ex.: author) for given source resource
# (ex.: article)
#
# @param source [DataMapper::Resource]
# source object (ex.: instance of article)
#
# @param target [DataMapper::Resource]
# target 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)
source_key_different = source_key_different?(source)
if (loaded?(source) && !source_key_different) || !valid_source?(source)
return
end
# SEL: load all related resources in the source collection
if source.saved? && (collection = source.collection).size > 1
eager_load(collection)
end
if !loaded?(source) || (source_key_dirty?(source) && source.saved?)
set!(source, resource_for(source))
elsif loaded?(source) && source_key_different
source_key.set(source, target_key.get!(get!(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)
raise ":nullable is deprecated, use :required instead (#{caller[2]})"
end
@required = options.fetch(:required, true)
@key = options.fetch(:key, false)
@unique = options.fetch(:unique, false)
@unique_index = options.fetch(:unique_index, @unique)
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
name = super
return name if name
name = DataMapper::Inflector.demodulize(source_model.name)
name = DataMapper::Inflector.underscore(name)
name = DataMapper::Inflector.pluralize(name)
name.to_sym
end
# @api private
def source_key_options(target_property)
DataMapper::Ext::Hash.only(target_property.options, :length, :precision, :scale).update(
:index => name,
:required => required?,
:key => key?,
:unique => @unique,
:unique_index => @unique_index
)
end
# @api private
def child_properties
source_key.map { |property| property.name }
end
# @api private
def source_key_different?(source)
source_key.get!(source) != target_key.get!(get!(source))
end
# @api private
def source_key_dirty?(source)
source.dirty_attributes.keys.any? { |property| source_key.include?(property) }
end
end # class Relationship
end # module ManyToOne
end # module Associations
end # module DataMapper