Fixing issues with dirty tracking on nested models and related improvements

This commit is contained in:
Sam Lown 2011-04-20 16:44:49 +02:00
parent 8c4505d191
commit 2cc119b3b3
15 changed files with 141 additions and 115 deletions

View file

@ -18,8 +18,8 @@ module CouchRest
include CouchRest::Model::Associations
include CouchRest::Model::Validations
include CouchRest::Model::Designs
include CouchRest::Model::Dirty
include CouchRest::Model::CastedBy
include CouchRest::Model::Dirty
def self.subclasses
@subclasses ||= []
@ -74,14 +74,6 @@ module CouchRest
super
end
### instance methods
# Checks if we're the top document
# (overrides base_doc? in casted_by.rb)
def base_doc?
!@casted_by
end
## Compatibility with ActiveSupport and older frameworks
# Hack so that CouchRest::Document, which descends from Hash,

View file

@ -5,25 +5,36 @@
module CouchRest::Model
class CastedArray < Array
include CouchRest::Model::CastedBy
include CouchRest::Model::Dirty
attr_accessor :casted_by
attr_accessor :property
attr_accessor :casted_by_property
def initialize(array, property)
self.property = property
def initialize(array, property, parent = nil)
self.casted_by_property = property
self.casted_by = parent unless parent.nil?
super(array)
end
# Adding new entries
def << obj
couchrest_parent_will_change! if use_dirty?
super(instantiate_and_cast(obj))
end
def push(obj)
couchrest_parent_will_change! if use_dirty?
super(instantiate_and_cast(obj))
end
def unshift(obj)
super(instantiate_and_cast(obj))
end
def []= index, obj
value = instantiate_and_cast(obj, false)
couchrest_parent_will_change! if use_dirty? && value != self[index]
super(index, value)
end
def pop
couchrest_parent_will_change! if use_dirty? && self.length > 0
super
@ -34,17 +45,6 @@ module CouchRest::Model
super
end
def unshift(obj)
couchrest_parent_will_change! if use_dirty?
super(instantiate_and_cast(obj))
end
def []= index, obj
value = instantiate_and_cast(obj)
couchrest_parent_will_change! if use_dirty? && value != self[index]
super(index, value)
end
def clear
couchrest_parent_will_change! if use_dirty? && self.length > 0
super
@ -52,11 +52,14 @@ module CouchRest::Model
protected
def instantiate_and_cast(obj)
if self.casted_by && self.property && obj.class != self.property.type_class
self.property.cast_value(self.casted_by, obj)
def instantiate_and_cast(obj, change = true)
property = casted_by_property
couchrest_parent_will_change! if change && use_dirty?
if casted_by && property && obj.class != property.type_class
property.cast_value(casted_by, obj)
else
obj.casted_by = self.casted_by if obj.respond_to?(:casted_by)
obj.casted_by = casted_by if obj.respond_to?(:casted_by)
obj.casted_by_property = casted_by_property if obj.respond_to?(:casted_by_property)
obj
end
end

View file

@ -4,6 +4,7 @@ module CouchRest::Model
extend ActiveSupport::Concern
included do
self.send(:attr_accessor, :casted_by)
self.send(:attr_accessor, :casted_by_property)
end
# Gets a reference to the actual document in the DB
@ -11,13 +12,22 @@ module CouchRest::Model
# Otherwise we're at the top and we return self
def base_doc
return self if base_doc?
@casted_by ? @casted_by.base_doc : nil
casted_by ? casted_by.base_doc : nil
end
# Checks if we're the top document
def base_doc?
false
!casted_by
end
# Provide the property this casted model instance has been
# used by. If it has not been set, search through the
# casted_by objects properties to try and find it.
#def casted_by_property
# return nil unless casted_by
# attrs = casted_by.attributes
# @casted_by_property ||= casted_by.properties.detect{ |k| attrs[k.to_s] === self }
#end
end
end

View file

@ -3,8 +3,16 @@
module CouchRest::Model
class CastedHash < Hash
include CouchRest::Model::CastedBy
include CouchRest::Model::Dirty
attr_accessor :casted_by
attr_accessor :casted_by_property
def self.[](hash, property, parent = nil)
obj = super(hash)
obj.casted_by_property = property
obj.casted_by = parent unless parent.nil?
obj
end
# needed for dirty
def attributes

View file

@ -1,6 +1,6 @@
module CouchRest::Model
module CastedModel
extend ActiveSupport::Concern
included do
@ -10,8 +10,14 @@ module CouchRest::Model
include CouchRest::Model::PropertyProtection
include CouchRest::Model::Associations
include CouchRest::Model::Validations
include CouchRest::Model::CastedBy
include CouchRest::Model::Dirty
# attr_accessor :casted_by
class_eval do
# Override CastedBy's base_doc?
def base_doc?
false # Can never be base doc!
end
end
end
def initialize(keys = {})
@ -21,7 +27,6 @@ module CouchRest::Model
end
def []= key, value
couchrest_attribute_will_change!(key) if self[key] != value
super(key.to_s, value)
end
@ -29,17 +34,10 @@ module CouchRest::Model
super(key.to_s)
end
# Gets a reference to the top level extended
# document that a model is saved inside of
def base_doc
return nil unless @casted_by
@casted_by.base_doc
end
# False if the casted model has already
# been saved in the containing document
def new?
@casted_by.nil? ? true : @casted_by.new?
casted_by.nil? ? true : casted_by.new?
end
alias :new_record? :new?
@ -68,4 +66,5 @@ module CouchRest::Model
alias :attributes= :update_attributes_without_saving
end
end

View file

@ -10,7 +10,6 @@ module CouchRest
# This applies to both Model::Base and Model::CastedModel
module Dirty
extend ActiveSupport::Concern
include CouchRest::Model::CastedBy # needed for base_doc
include ActiveModel::Dirty
included do
@ -21,8 +20,8 @@ module CouchRest
end
def use_dirty?
bdoc = base_doc
bdoc && !bdoc.disable_dirty
doc = base_doc
doc && !doc.disable_dirty
end
def couchrest_attribute_will_change!(attr)
@ -32,16 +31,7 @@ module CouchRest
end
def couchrest_parent_will_change!
@casted_by.couchrest_attribute_will_change!(casted_by_attribute) if @casted_by
end
private
# return the attribute name this object is referenced by in the parent
def casted_by_attribute
return @casted_by_attribute if @casted_by_attribute
attr = @casted_by.attributes
@casted_by_attribute = attr.keys.detect { |k| attr[k] == self }
casted_by.couchrest_attribute_will_change!(casted_by_property.name) if casted_by_property
end
end

View file

@ -30,7 +30,7 @@ module CouchRest
def update(options = {})
raise "Calling #{self.class.name}#update on document that has not been created!" if self.new?
return false unless perform_validations(options)
return true if !self.changed?
return true if !self.disable_dirty && !self.changed?
_run_update_callbacks do
_run_save_callbacks do
result = database.save_doc(self)
@ -143,20 +143,14 @@ module CouchRest
# must be globally unique across all document types which share a
# database, so if you'd like to scope uniqueness to this class, you
# should use the class name as part of the unique id.
def unique_id method = nil, &block
def unique_id(method = nil, &block)
if method
define_method :get_unique_id do
self.send(method)
end
define_method :set_unique_id do
self['_id'] ||= get_unique_id
self['_id'] ||= self.send(method)
end
elsif block
define_method :get_unique_id do
block.call(self)
end
define_method :set_unique_id do
uniqid = get_unique_id
uniqid = block.call(self)
raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
self['_id'] ||= uniqid
end

View file

@ -6,9 +6,9 @@ module CouchRest
included do
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
extlib_inheritable_accessor(:property_by_name) unless self.respond_to?(:property_by_name)
extlib_inheritable_accessor(:properties_by_name) unless self.respond_to?(:properties_by_name)
self.properties ||= []
self.property_by_name ||= {}
self.properties_by_name ||= {}
raise "You can only mixin Properties in a class responding to [] and []=, if you tried to mixin CastedModel, make sure your class inherits from Hash or responds to the proper methods" unless (method_defined?(:[]) && method_defined?(:[]=))
end
@ -20,6 +20,12 @@ module CouchRest
self.class.properties
end
# Returns all the class's properties as a Hash where the key is the name
# of the property.
def properties_by_name
self.class.properties_by_name
end
# Returns the Class properties with their values
#
# ==== Returns
@ -43,31 +49,10 @@ module CouchRest
def write_attribute(property, value)
prop = find_property!(property)
value = prop.is_a?(String) ? value : prop.cast(self, value)
attribute_will_change!(prop.name) if use_dirty? && self[prop.name] != value
couchrest_attribute_will_change!(prop.name) if use_dirty? && self[prop.name] != value
self[prop.name] = value
end
def []=(key,value)
return super(key,value) unless use_dirty?
has_changes = self.changed?
if !has_changes && self.respond_to?(:get_unique_id)
check_id_change = true
old_id = get_unique_id
end
ret = super(key, value)
if check_id_change
# if we have set an attribute that results in the _id changing (unique_id),
# force changed? to return true so that the record can be saved
new_id = get_unique_id
changed_attributes["_id"] = new_id if old_id != new_id
end
ret
end
# Takes a hash as argument, and applies the values by using writer methods
# for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
# missing. In case of error, no attributes are changed.
@ -90,7 +75,7 @@ module CouchRest
protected
def find_property(property)
property.is_a?(Property) ? property : self.class.property_by_name[property.to_s]
property.is_a?(Property) ? property : self.class.properties_by_name[property.to_s]
end
# The following methods should be accessable by the Model::Base Class, but not by anything else!
@ -212,7 +197,7 @@ module CouchRest
validates_casted_model property.name
end
properties << property
property_by_name[property.to_s] = property
properties_by_name[property.to_s] = property
property
end

View file

@ -38,16 +38,13 @@ module CouchRest::Model
end
arr = value.collect { |data| cast_value(parent, data) }
# allow casted_by calls to be passed up chain by wrapping in CastedArray
value = CastedArray.new(arr, self)
value.casted_by = parent
CastedArray.new(arr, self, parent)
elsif (type == Object || type == Hash) && (value.class == Hash)
# allow casted_by calls to be passed up chain by wrapping in CastedHash
value = CouchRest::Model::CastedHash[value]
value.casted_by = parent
CastedHash[value, self, parent]
elsif !value.nil?
value = cast_value(parent, value)
cast_value(parent, value)
end
value
end
# Cast an individual value, not an array
@ -71,6 +68,7 @@ module CouchRest::Model
def associate_casted_value_to_parent(parent, value)
value.casted_by = parent if value.respond_to?(:casted_by)
value.casted_by_property = self if value.respond_to?(:casted_by_property)
value
end