lib/dm-core/associations/one_to_many.rb
module DataMapper
module Associations
module OneToMany #:nodoc:
class Relationship < Associations::Relationship
# @api semipublic
alias_method :target_repository_name, :child_repository_name
# @api semipublic
alias_method :target_model, :child_model
# @api semipublic
alias_method :source_repository_name, :parent_repository_name
# @api semipublic
alias_method :source_model, :parent_model
# @api semipublic
alias_method :source_key, :parent_key
# @api semipublic
def child_key
inverse.child_key
end
# @api semipublic
alias_method :target_key, :child_key
# Returns a Collection 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 [Collection]
# The collection scoped to the relationship, source and query
#
# @api private
def collection_for(source, other_query = nil)
query = query_for(source, other_query)
collection = collection_class.new(query)
collection.relationship = self
collection.source = source
# make the collection empty if the source is new
collection.replace([]) if source.new?
collection
end
# Loads and returns association targets (ex.: articles) for given source resource
# (ex.: author)
#
# @api semipublic
def get(source, query = nil)
lazy_load(source)
collection = get_collection(source)
query ? collection.all(query) : collection
end
# @api private
def get_collection(source)
get!(source)
end
# Sets value of association targets (ex.: paragraphs) for given source resource
# (ex.: article)
#
# @api semipublic
def set(source, targets)
lazy_load(source)
get!(source).replace(targets)
end
# @api private
def set_collection(source, target)
set!(source, target)
end
# Loads association targets 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)
# SEL: load all related resources in the source collection
if source.saved? && (collection = source.collection).size > 1
eager_load(collection)
end
unless loaded?(source)
set!(source, collection_for(source))
end
end
# initialize the inverse "many to one" relationships explicitly before
# initializing other relationships. This makes sure that foreign key
# properties always appear in the order they were declared.
#
# @return [self]
#
# @api public
def finalize
child_model.relationships.each do |relationship|
# TODO: should this check #inverse?
# relationship.child_key if inverse?(relationship)
if relationship.kind_of?(Associations::ManyToOne::Relationship)
relationship.finalize
end
end
inverse.finalize
self
end
# @api semipublic
def default_for(source)
collection_for(source).replace(Array(super))
end
private
# @api semipublic
def initialize(name, target_model, source_model, options = {})
target_model ||= DataMapper::Inflector.camelize(DataMapper::Inflector.singularize(name.to_s))
options = { :min => 0, :max => source_model.n }.update(options)
super
end
# Sets the association targets in the resource
#
# @param [Resource] source
# the source to set
# @param [Array<Resource>] targets
# the target collection for the association
# @param [Query, Hash] query
# the query to scope the association with
#
# @return [undefined]
#
# @api private
def eager_load_targets(source, targets, query)
set!(source, collection_for(source, query).set(targets))
end
# Returns collection class used by this type of
# relationship
#
# @api private
def collection_class
OneToMany::Collection
end
# Returns the inverse relationship class
#
# @api private
def inverse_class
ManyToOne::Relationship
end
# Returns the inverse relationship name
#
# @api private
def inverse_name
super || DataMapper::Inflector.underscore(DataMapper::Inflector.demodulize(source_model.name)).to_sym
end
# @api private
def child_properties
super || parent_key.map do |parent_property|
"#{inverse_name}_#{parent_property.name}".to_sym
end
end
end # class Relationship
class Collection < DataMapper::Collection
# @api private
attr_accessor :relationship
# @api private
attr_accessor :source
# @api public
def reload(*)
assert_source_saved 'The source must be saved before reloading the collection'
super
end
# Replace the Resources within the 1:m Collection
#
# @param [Enumerable] other
# List of other Resources to replace with
#
# @return [Collection]
# self
#
# @api public
def replace(*)
lazy_load # lazy load so that targets are always orphaned
super
end
# Removes all Resources from the 1:m Collection
#
# This should remove and orphan each Resource from the 1:m Collection.
#
# @return [Collection]
# self
#
# @api public
def clear
lazy_load # lazy load so that targets are always orphaned
super
end
# Update every Resource in the 1:m Collection
#
# @param [Hash] attributes
# attributes to update with
#
# @return [Boolean]
# true if the resources were successfully updated
#
# @api public
def update(*)
assert_source_saved 'The source must be saved before mass-updating the collection'
super
end
# Update every Resource in the 1:m Collection, bypassing validation
#
# @param [Hash] attributes
# attributes to update
#
# @return [Boolean]
# true if the resources were successfully updated
#
# @api public
def update!(*)
assert_source_saved 'The source must be saved before mass-updating the collection'
super
end
# Remove every Resource in the 1:m Collection from the repository
#
# This performs a deletion of each Resource in the Collection from
# the repository and clears the Collection.
#
# @return [Boolean]
# true if the resources were successfully destroyed
#
# @api public
def destroy
assert_source_saved 'The source must be saved before mass-deleting the collection'
super
end
# Remove every Resource in the 1:m Collection from the repository, bypassing validation
#
# This performs a deletion of each Resource in the Collection from
# the repository and clears the Collection while skipping
# validation.
#
# @return [Boolean]
# true if the resources were successfully destroyed
#
# @api public
def destroy!
assert_source_saved 'The source must be saved before mass-deleting the collection'
super
end
private
# @api private
def _create(*)
assert_source_saved 'The source must be saved before creating a resource'
super
end
# @api private
def _save(execute_hooks = true)
assert_source_saved 'The source must be saved before saving the collection'
# update removed resources to not reference the source
@removed.all? { |resource| resource.destroyed? || resource.__send__(execute_hooks ? :save : :save!) } && super
end
# @api private
def lazy_load
if source.saved?
super
end
end
# @api private
def new_collection(query, resources = nil, &block)
collection = self.class.new(query, &block)
collection.relationship = relationship
collection.source = source
resources ||= filter(query) if loaded?
# set the resources after the relationship and source are set
if resources
collection.set(resources)
end
collection
end
# @api private
def resource_added(resource)
resource = initialize_resource(resource)
inverse_set(resource, source)
super
end
# @api private
def resource_removed(resource)
inverse_set(resource, nil)
super
end
# @api private
def inverse_set(source, target)
unless source.readonly?
relationship.inverse.set(source, target)
end
end
# @api private
def assert_source_saved(message)
unless source.saved?
raise UnsavedParentError, message
end
end
end # class Collection
end # module OneToMany
end # module Associations
end # module DataMapper