Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

1214 lines (1098 sloc) 32.05 kb
module DataMapper
module Resource
include DataMapper::Assertions
# Return if Resource#save should raise an exception on save failures (per-resource)
#
# This delegates to model.raise_on_save_failure by default.
#
# user.raise_on_save_failure # => false
#
# @return [Boolean]
# true if a failure in Resource#save should raise an exception
#
# @api public
def raise_on_save_failure
if defined?(@raise_on_save_failure)
@raise_on_save_failure
else
model.raise_on_save_failure
end
end
# Specify if Resource#save should raise an exception on save failures (per-resource)
#
# @param [Boolean]
# a boolean that if true will cause Resource#save to raise an exception
#
# @return [Boolean]
# true if a failure in Resource#save should raise an exception
#
# @api public
def raise_on_save_failure=(raise_on_save_failure)
@raise_on_save_failure = raise_on_save_failure
end
# Deprecated API for updating attributes and saving Resource
#
# @see #update
#
# @deprecated
def update_attributes(attributes = {}, *allowed)
raise "#{model}#update_attributes is deprecated, use #{model}#update instead (#{caller.first})"
end
# Makes sure a class gets all the methods when it includes Resource
#
# Note that including this module into an anonymous class will leave
# the model descendant tracking mechanism with no possibility to reliably
# track the anonymous model across code reloads. This means that
# {DataMapper::DescendantSet} will currently leak memory in scenarios where
# anonymous models are reloaded multiple times (as is the case in dm-rails
# development mode for example).
#
# @api private
def self.included(model)
model.extend Model
end
# @api public
alias_method :model, :class
# Get the persisted state for the resource
#
# @return [Resource::PersistenceState]
# the current persisted state for the resource
#
# @api private
def persistence_state
@_persistence_state ||= Resource::PersistenceState::Transient.new(self)
end
# Set the persisted state for the resource
#
# @param [Resource::PersistenceState]
# the new persisted state for the resource
#
# @return [undefined]
#
# @api private
def persistence_state=(state)
@_persistence_state = state
end
# Test if the persisted state is set
#
# @return [Boolean]
# true if the persisted state is set
#
# @api private
def persistence_state?
defined?(@_persistence_state) ? true : false
end
# Repository this resource belongs to in the context of this collection
# or of the resource's class.
#
# @return [Repository]
# the respository this resource belongs to, in the context of
# a collection OR in the instance's Model's context
#
# @api semipublic
def repository
# only set @_repository explicitly when persisted
defined?(@_repository) ? @_repository : model.repository
end
# Retrieve the key(s) for this resource.
#
# This always returns the persisted key value,
# even if the key is changed and not yet persisted.
# This is done so all relations still work.
#
# @return [Array(Key)]
# the key(s) identifying this resource
#
# @api public
def key
return @_key if defined?(@_key)
model_key = model.key(repository_name)
key = model_key.map do |property|
original_attributes[property] || (property.loaded?(self) ? property.get!(self) : nil)
end
# only memoize a valid key
@_key = key if model_key.valid?(key)
end
# Checks if this Resource instance is new
#
# @return [Boolean]
# true if the resource is new and not saved
#
# @api public
def new?
persistence_state.kind_of?(PersistenceState::Transient)
end
# Checks if this Resource instance is saved
#
# @return [Boolean]
# true if the resource has been saved
#
# @api public
def saved?
persistence_state.kind_of?(PersistenceState::Persisted)
end
# Checks if this Resource instance is destroyed
#
# @return [Boolean]
# true if the resource has been destroyed
#
# @api public
def destroyed?
readonly? && !key.nil?
end
# Checks if the resource has no changes to save
#
# @return [Boolean]
# true if the resource may not be persisted
#
# @api public
def clean?
persistence_state.kind_of?(PersistenceState::Clean) ||
persistence_state.kind_of?(PersistenceState::Immutable)
end
# Checks if the resource has unsaved changes
#
# @return [Boolean]
# true if resource may be persisted
#
# @api public
def dirty?
run_once(true) do
dirty_self? || dirty_parents? || dirty_children?
end
end
# Checks if this Resource instance is readonly
#
# @return [Boolean]
# true if the resource cannot be persisted
#
# @api public
def readonly?
persistence_state.kind_of?(PersistenceState::Immutable)
end
# Returns the value of the attribute.
#
# Do not read from instance variables directly, but use this method.
# This method handles lazy loading the attribute and returning of
# defaults if nessesary.
#
# @example
# class Foo
# include DataMapper::Resource
#
# property :first_name, String
# property :last_name, String
#
# def full_name
# "#{attribute_get(:first_name)} #{attribute_get(:last_name)}"
# end
#
# # using the shorter syntax
# def name_for_address_book
# "#{last_name}, #{first_name}"
# end
# end
#
# @param [Symbol] name
# name of attribute to retrieve
#
# @return [Object]
# the value stored at that given attribute
# (nil if none, and default if necessary)
#
# @api public
def attribute_get(name)
property = properties[name]
persistence_state.get(property) if property
end
alias_method :[], :attribute_get
# Sets the value of the attribute and marks the attribute as dirty
# if it has been changed so that it may be saved. Do not set from
# instance variables directly, but use this method. This method
# handles the lazy loading the property and returning of defaults
# if nessesary.
#
# @example
# class Foo
# include DataMapper::Resource
#
# property :first_name, String
# property :last_name, String
#
# def full_name(name)
# name = name.split(' ')
# attribute_set(:first_name, name[0])
# attribute_set(:last_name, name[1])
# end
#
# # using the shorter syntax
# def name_from_address_book(name)
# name = name.split(', ')
# self.first_name = name[1]
# self.last_name = name[0]
# end
# end
#
# @param [Symbol] name
# name of attribute to set
# @param [Object] value
# value to store
#
# @return [undefined]
#
# @api public
def attribute_set(name, value)
property = properties[name]
self.persistence_state = persistence_state.set(property, value) if property
end
alias_method :[]=, :attribute_set
# Gets all the attributes of the Resource instance
#
# @param [Symbol] key_on
# Use this attribute of the Property as keys.
# defaults to :name. :field is useful for adapters
# :property or nil use the actual Property object.
#
# @return [Hash]
# All the attributes
#
# @api public
def attributes(key_on = :name)
attributes = {}
lazy_load(properties)
fields.each do |property|
if model.public_method_defined?(name = property.name)
key = case key_on
when :name then name
when :field then property.field
else property
end
attributes[key] = __send__(name)
end
end
attributes
end
# Assign values to multiple attributes in one call (mass assignment)
#
# @param [Hash] attributes
# names and values of attributes to assign
#
# @return [Hash]
# names and values of attributes assigned
#
# @api public
def attributes=(attributes)
model = self.model
attributes.each do |name, value|
case name
when String, Symbol
if model.allowed_writer_methods.include?(setter = "#{name}=")
__send__(setter, value)
else
raise ArgumentError, "The attribute '#{name}' is not accessible in #{model}"
end
when Associations::Relationship, Property
self.persistence_state = persistence_state.set(name, value)
end
end
end
# Reloads association and all child association
#
# This is accomplished by resetting the Resource key to it's
# original value, and then removing all the ivars for properties
# and relationships. On the next access of those ivars, the
# resource will eager load what it needs. While this is more of
# a lazy reload, it should result in more consistent behavior
# since no cached results will remain from the initial load.
#
# @return [Resource]
# the receiver, the current Resource instance
#
# @api public
def reload
if key
reset_key
clear_subjects
end
self.persistence_state = persistence_state.rollback
self
end
# Updates attributes and saves this Resource instance
#
# @param [Hash] attributes
# attributes to be updated
#
# @return [Boolean]
# true if resource and storage state match
#
# @api public
def update(attributes)
assert_update_clean_only(:update)
self.attributes = attributes
save
end
# Updates attributes and saves this Resource instance, bypassing hooks
#
# @param [Hash] attributes
# attributes to be updated
#
# @return [Boolean]
# true if resource and storage state match
#
# @api public
def update!(attributes)
assert_update_clean_only(:update!)
self.attributes = attributes
save!
end
# Save the instance and loaded, dirty associations to the data-store
#
# @return [Boolean]
# true if Resource instance and all associations were saved
#
# @api public
def save
assert_not_destroyed(:save)
retval = _save
assert_save_successful(:save, retval)
retval
end
# Save the instance and loaded, dirty associations to the data-store, bypassing hooks
#
# @return [Boolean]
# true if Resource instance and all associations were saved
#
# @api public
def save!
assert_not_destroyed(:save!)
retval = _save(false)
assert_save_successful(:save!, retval)
retval
end
# Destroy the instance, remove it from the repository
#
# @return [Boolean]
# true if resource was destroyed
#
# @api public
def destroy
return true if destroyed?
catch :halt do
before_destroy_hook
_destroy
after_destroy_hook
end
destroyed?
end
# Destroy the instance, remove it from the repository, bypassing hooks
#
# @return [Boolean]
# true if resource was destroyed
#
# @api public
def destroy!
return true if destroyed?
_destroy(false)
destroyed?
end
# Compares another Resource for equality
#
# Resource is equal to +other+ if they are the same object
# (identical object_id) or if they are both of the *same model* and
# all of their attributes are equivalent
#
# @param [Resource] other
# the other Resource to compare with
#
# @return [Boolean]
# true if they are equal, false if not
#
# @api public
def eql?(other)
return true if equal?(other)
instance_of?(other.class) && cmp?(other, :eql?)
end
# Compares another Resource for equivalency
#
# Resource is equivalent to +other+ if they are the same object
# (identical object_id) or all of their attribute are equivalent
#
# @param [Resource] other
# the other Resource to compare with
#
# @return [Boolean]
# true if they are equivalent, false if not
#
# @api public
def ==(other)
return true if equal?(other)
return false unless other.kind_of?(Resource) && model.base_model.equal?(other.model.base_model)
cmp?(other, :==)
end
# Compares two Resources to allow them to be sorted
#
# @param [Resource] other
# The other Resource to compare with
#
# @return [Integer]
# Return 0 if Resources should be sorted as the same, -1 if the
# other Resource should be after self, and 1 if the other Resource
# should be before self
#
# @api public
def <=>(other)
model = self.model
unless other.kind_of?(model.base_model)
raise ArgumentError, "Cannot compare a #{other.class} instance with a #{model} instance"
end
model.default_order(repository_name).each do |direction|
cmp = direction.get(self) <=> direction.get(other)
return cmp if cmp.nonzero?
end
0
end
# Returns hash value of the object.
# Two objects with the same hash value assumed equal (using eql? method)
#
# DataMapper resources are equal when their models have the same hash
# and they have the same set of properties
#
# When used as key in a Hash or Hash subclass, objects are compared
# by eql? and thus hash value has direct effect on lookup
#
# @api private
def hash
model.hash ^ key.hash
end
# Get a Human-readable representation of this Resource instance
#
# Foo.new #=> #<Foo name=nil updated_at=nil created_at=nil id=nil>
#
# @return [String]
# Human-readable representation of this Resource instance
#
# @api public
def inspect
# TODO: display relationship values
attrs = properties.map do |property|
value = if new? || property.loaded?(self)
property.get!(self).inspect
else
'<not loaded>'
end
"#{property.instance_variable_name}=#{value}"
end
"#<#{model.name} #{attrs.join(' ')}>"
end
# Hash of original values of attributes that have unsaved changes
#
# @return [Hash]
# original values of attributes that have unsaved changes
#
# @api semipublic
def original_attributes
if persistence_state.respond_to?(:original_attributes)
persistence_state.original_attributes.dup.freeze
else
{}.freeze
end
end
# Checks if an attribute has been loaded from the repository
#
# @example
# class Foo
# include DataMapper::Resource
#
# property :name, String
# property :description, Text, :lazy => false
# end
#
# Foo.new.attribute_loaded?(:description) #=> false
#
# @return [Boolean]
# true if ivar +name+ has been loaded
#
# @return [Boolean]
# true if ivar +name+ has been loaded
#
# @api private
def attribute_loaded?(name)
properties[name].loaded?(self)
end
# Checks if an attribute has unsaved changes
#
# @param [Symbol] name
# name of attribute to check for unsaved changes
#
# @return [Boolean]
# true if attribute has unsaved changes
#
# @api semipublic
def attribute_dirty?(name)
dirty_attributes.key?(properties[name])
end
# Hash of attributes that have unsaved changes
#
# @return [Hash]
# attributes that have unsaved changes
#
# @api semipublic
def dirty_attributes
dirty_attributes = {}
original_attributes.each_key do |property|
next unless property.respond_to?(:dump)
dirty_attributes[property] = property.dump(property.get!(self))
end
dirty_attributes
end
# Returns the Collection the Resource is associated with
#
# @return [nil]
# nil if this is a new record
# @return [Collection]
# a Collection that self belongs to
#
# @api private
def collection
return @_collection if @_collection || new? || readonly?
collection_for_self
end
# Associates a Resource to a Collection
#
# @param [Collection, nil] collection
# the collection to associate the resource with
#
# @return [nil]
# nil if this is a new record
# @return [Collection]
# a Collection that self belongs to
#
# @api private
def collection=(collection)
@_collection = collection
end
# Return a collection including the current resource only
#
# @return [Collection]
# a collection containing self
#
# @api private
def collection_for_self
Collection.new(query, [ self ])
end
# Returns a Query that will match the resource
#
# @return [Query]
# Query that will match the resource
#
# @api semipublic
def query
repository.new_query(model, :fields => fields, :conditions => conditions)
end
protected
# Method for hooking callbacks before resource saving
#
# @return [undefined]
#
# @api private
def before_save_hook
execute_hooks_for(:before, :save)
end
# Method for hooking callbacks after resource saving
#
# @return [undefined]
#
# @api private
def after_save_hook
execute_hooks_for(:after, :save)
end
# Method for hooking callbacks before resource creation
#
# @return [undefined]
#
# @api private
def before_create_hook
execute_hooks_for(:before, :create)
end
# Method for hooking callbacks after resource creation
#
# @return [undefined]
#
# @api private
def after_create_hook
execute_hooks_for(:after, :create)
end
# Method for hooking callbacks before resource updating
#
# @return [undefined]
#
# @api private
def before_update_hook
execute_hooks_for(:before, :update)
end
# Method for hooking callbacks after resource updating
#
# @return [undefined]
#
# @api private
def after_update_hook
execute_hooks_for(:after, :update)
end
# Method for hooking callbacks before resource destruction
#
# @return [undefined]
#
# @api private
def before_destroy_hook
execute_hooks_for(:before, :destroy)
end
# Method for hooking callbacks after resource destruction
#
# @return [undefined]
#
# @api private
def after_destroy_hook
execute_hooks_for(:after, :destroy)
end
private
# Initialize a new instance of this Resource using the provided values
#
# @param [Hash] attributes
# attribute values to use for the new instance
#
# @return [Hash]
# attribute values used in the new instance
#
# @api public
def initialize(attributes = nil) # :nodoc:
self.attributes = attributes if attributes
end
# @api private
def initialize_copy(original)
instance_variables.each do |ivar|
instance_variable_set(ivar, DataMapper::Ext.try_dup(instance_variable_get(ivar)))
end
self.persistence_state = persistence_state.class.new(self)
end
# Returns name of the repository this object
# was loaded from
#
# @return [String]
# name of the repository this object was loaded from
#
# @api private
def repository_name
repository.name
end
# Gets this instance's Model's properties
#
# @return [PropertySet]
# List of this Resource's Model's properties
#
# @api private
def properties
model.properties(repository_name)
end
# Gets this instance's Model's relationships
#
# @return [RelationshipSet]
# List of this instance's Model's Relationships
#
# @api private
def relationships
model.relationships(repository_name)
end
# Returns the identity map for the model from the repository
#
# @return [IdentityMap]
# identity map of repository this object was loaded from
#
# @api private
def identity_map
repository.identity_map(model)
end
# @api private
def add_to_identity_map
identity_map[key] = self
end
# @api private
def remove_from_identity_map
identity_map.delete(key)
end
# Fetches all the names of the attributes that have been loaded,
# even if they are lazy but have been called
#
# @return [Array<Property>]
# names of attributes that have been loaded
#
# @api private
def fields
properties.select do |property|
property.loaded?(self) || (new? && property.default?)
end
end
# Reset the key to the original value
#
# @return [undefined]
#
# @api private
def reset_key
properties.key.zip(key) do |property, value|
property.set!(self, value)
end
end
# Remove all the ivars for properties and relationships
#
# @return [undefined]
#
# @api private
def clear_subjects
model_properties = properties
(model_properties - model_properties.key | relationships).each do |subject|
next unless subject.loaded?(self)
remove_instance_variable(subject.instance_variable_name)
end
end
# Lazy loads attributes not yet loaded
#
# @param [Array<Property>] properties
# the properties to reload
#
# @return [self]
#
# @api private
def lazy_load(properties)
eager_load(properties - fields)
end
# Reloads specified attributes
#
# @param [Array<Property>] properties
# the properties to reload
#
# @return [Resource]
# the receiver, the current Resource instance
#
# @api private
def eager_load(properties)
unless properties.empty? || key.nil? || collection.nil?
# set an initial value to prevent recursive lazy loads
properties.each { |property| property.set!(self, nil) }
collection.reload(:fields => properties)
end
self
end
# Return conditions to match the Resource
#
# @return [Hash]
# query conditions
#
# @api private
def conditions
key = self.key
if key
model.key_conditions(repository, key)
else
conditions = {}
properties.each do |property|
next unless property.loaded?(self)
conditions[property] = property.get!(self)
end
conditions
end
end
# @api private
def parent_relationships
parent_relationships = []
relationships.each do |relationship|
next unless relationship.respond_to?(:resource_for)
set_default_value(relationship)
next unless relationship.loaded?(self) && relationship.get!(self)
parent_relationships << relationship
end
parent_relationships
end
# Returns loaded child relationships
#
# @return [Array<Associations::OneToMany::Relationship>]
# array of child relationships for which this resource is parent and is loaded
#
# @api private
def child_relationships
child_relationships = []
relationships.each do |relationship|
next unless relationship.respond_to?(:collection_for)
set_default_value(relationship)
next unless relationship.loaded?(self)
child_relationships << relationship
end
many_to_many, other = child_relationships.partition do |relationship|
relationship.kind_of?(Associations::ManyToMany::Relationship)
end
many_to_many + other
end
# @api private
def parent_associations
parent_relationships.map { |relationship| relationship.get!(self) }
end
# @api private
def child_associations
child_relationships.map { |relationship| relationship.get_collection(self) }
end
# Commit the persisted state
#
# @return [undefined]
#
# @api private
def _persist
self.persistence_state = persistence_state.commit
end
# This method executes the hooks before and after resource creation
#
# @return [Boolean]
#
# @see Resource#_create
#
# @api private
def create_with_hooks
catch :halt do
before_save_hook
before_create_hook
_persist
after_create_hook
after_save_hook
end
end
# This method executes the hooks before and after resource updating
#
# @return [Boolean]
#
# @see Resource#_update
#
# @api private
def update_with_hooks
catch :halt do
before_save_hook
before_update_hook
_persist
after_update_hook
after_save_hook
end
end
# Destroy the resource
#
# @return [undefined]
#
# @api private
def _destroy(execute_hooks = true)
self.persistence_state = persistence_state.delete
_persist
end
# @api private
def _save(execute_hooks = true)
run_once(true) do
save_parents(execute_hooks) && save_self(execute_hooks) && save_children(execute_hooks)
end
end
# Saves the resource
#
# @return [Boolean]
# true if the resource was successfully saved
#
# @api semipublic
def save_self(execute_hooks = true)
# short-circuit if the resource is not dirty
return saved? unless dirty_self?
if execute_hooks
new? ? create_with_hooks : update_with_hooks
else
_persist
end
clean?
end
# Saves the parent resources
#
# @return [Boolean]
# true if the parents were successfully saved
#
# @api private
def save_parents(execute_hooks)
run_once(true) do
parent_relationships.map do |relationship|
parent = relationship.get(self)
if parent.__send__(:save_parents, execute_hooks) && parent.__send__(:save_self, execute_hooks)
relationship.set(self, parent) # set the FK values
end
end.all?
end
end
# Saves the children resources
#
# @return [Boolean]
# true if the children were successfully saved
#
# @api private
def save_children(execute_hooks)
child_associations.map do |association|
association.__send__(execute_hooks ? :save : :save!)
end.all?
end
# Checks if the resource has unsaved changes
#
# @return [Boolean]
# true if the resource has unsaved changes
#
# @api semipublic
def dirty_self?
if original_attributes.any?
true
elsif new?
!model.serial.nil? || properties.any? { |property| property.default? }
else
false
end
end
# Checks if the parents have unsaved changes
#
# @return [Boolean]
# true if the parents have unsaved changes
#
# @api private
def dirty_parents?
run_once(false) do
parent_associations.any? do |association|
association.__send__(:dirty_self?) || association.__send__(:dirty_parents?)
end
end
end
# Checks if the children have unsaved changes
#
# @param [Hash] resources
# resources that have already been tested
#
# @return [Boolean]
# true if the children have unsaved changes
#
# @api private
def dirty_children?
child_associations.any? { |association| association.dirty? }
end
# Return true if +other+'s is equivalent or equal to +self+'s
#
# @param [Resource] other
# The Resource whose attributes are to be compared with +self+'s
# @param [Symbol] operator
# The comparison operator to use to compare the attributes
#
# @return [Boolean]
# The result of the comparison of +other+'s attributes with +self+'s
#
# @api private
def cmp?(other, operator)
return false unless repository.send(operator, other.repository) &&
key.send(operator, other.key)
if saved? && other.saved?
# if dirty attributes match then they are the same resource
dirty_attributes == other.dirty_attributes
else
# compare properties for unsaved resources
properties.all? do |property|
__send__(property.name).send(operator, other.__send__(property.name))
end
end
end
# @api private
def set_default_value(subject)
return unless persistence_state.respond_to?(:set_default_value, true)
persistence_state.__send__(:set_default_value, subject)
end
# Execute all the queued up hooks for a given type and name
#
# @param [Symbol] type
# the type of hook to execute (before or after)
# @param [Symbol] name
# the name of the hook to execute
#
# @return [undefined]
#
# @api private
def execute_hooks_for(type, name)
model.hooks[name][type].each { |hook| hook.call(self) }
end
# Raises an exception if #update is performed on a dirty resource
#
# @param [Symbol] method
# the name of the method to use in the exception
#
# @return [undefined]
#
# @raise [UpdateConflictError]
# raise if the resource is dirty
#
# @api private
def assert_update_clean_only(method)
if dirty?
raise UpdateConflictError, "#{model}##{method} cannot be called on a #{new? ? 'new' : 'dirty'} resource"
end
end
# Raises an exception if #save is performed on a destroyed resource
#
# @param [Symbol] method
# the name of the method to use in the exception
#
# @return [undefined]
#
# @raise [PersistenceError]
# raise if the resource is destroyed
#
# @api private
def assert_not_destroyed(method)
if destroyed?
raise PersistenceError, "#{model}##{method} cannot be called on a destroyed resource"
end
end
# Raises an exception if #save returns false
#
# @param [Symbol] method
# the name of the method to use in the exception
# @param [Boolean] save_result
# the result of the #save call
#
# @return [undefined]
#
# @raise [SaveFailureError]
# raise if the resource was not saved
#
# @api private
def assert_save_successful(method, save_retval)
if save_retval != true && raise_on_save_failure
raise SaveFailureError.new("#{model}##{method} returned #{save_retval.inspect}, #{model} was not saved", self)
end
end
# Prevent a method from being in the stack more than once
#
# The purpose of this method is to prevent SystemStackError from
# being thrown from methods from encountering infinite recursion
# when called on resources having circular dependencies.
#
# @param [Object] default
# default return value
#
# @yield The block of code to run once
#
# @return [Object]
# block return value
#
# @api private
def run_once(default)
caller_method = Kernel.caller(1).first[/`([^'?!]+)[?!]?'/, 1]
sentinel = "@_#{caller_method}_sentinel"
return instance_variable_get(sentinel) if instance_variable_defined?(sentinel)
begin
instance_variable_set(sentinel, default)
yield
ensure
remove_instance_variable(sentinel)
end
end
end # module Resource
end # module DataMapper
Jump to Line
Something went wrong with that request. Please try again.