Skip to content

Commit

Permalink
Fixing issues with dirty tracking on nested models and related improv…
Browse files Browse the repository at this point in the history
…ements
  • Loading branch information
samlown committed Apr 20, 2011
1 parent 8c4505d commit 2cc119b
Show file tree
Hide file tree
Showing 15 changed files with 141 additions and 115 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.0.beta3
1.1.0.beta4
2 changes: 1 addition & 1 deletion couchrest_model.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Gem::Specification.new do |s|

s.add_dependency(%q<couchrest>, "1.1.0.pre2")
s.add_dependency(%q<mime-types>, "~> 1.15")
s.add_dependency(%q<activemodel>, "~> 3.0.0")
s.add_dependency(%q<activemodel>, "~> 3.0.5")
s.add_dependency(%q<tzinfo>, "~> 0.3.22")
s.add_dependency(%q<railties>, "~> 3.0.0")
s.add_development_dependency(%q<rspec>, ">= 2.0.0")
Expand Down
6 changes: 5 additions & 1 deletion history.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
== 1.1.0.beta3
== 1.1.0.beta4

* Major changes:
* Fast Dirty Tracking! Many thanks to @sobakasu (Andrew Williams)
Expand All @@ -10,6 +10,10 @@
* Added "auto_update_design_doc" configuration option.
* Using #descending on View object will automatically swap startkey with endkey.

== 1.1.0.beta3

* Removed

== 1.1.0.beta2

* Minor enhancements:
Expand Down
10 changes: 1 addition & 9 deletions lib/couchrest/model/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ class Base < Document
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 ||= []
Expand Down Expand Up @@ -74,14 +74,6 @@ def self.method_missing(m, *args, &block)
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,
Expand Down
49 changes: 26 additions & 23 deletions lib/couchrest/model/casted_array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,29 +45,21 @@ def shift
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
end

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
Expand Down
14 changes: 12 additions & 2 deletions lib/couchrest/model/casted_by.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,30 @@ module CastedBy
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
# Calls up to the next document if there is one,
# 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
10 changes: 9 additions & 1 deletion lib/couchrest/model/casted_hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 10 additions & 11 deletions lib/couchrest/model/casted_model.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module CouchRest::Model
module CastedModel

extend ActiveSupport::Concern

included do
Expand All @@ -10,8 +10,14 @@ module CastedModel
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 = {})
Expand All @@ -21,25 +27,17 @@ def initialize(keys = {})
end

def []= key, value
couchrest_attribute_will_change!(key) if self[key] != value
super(key.to_s, value)
end

def [] key
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?

Expand Down Expand Up @@ -68,4 +66,5 @@ def update_attributes_without_saving(hash)
alias :attributes= :update_attributes_without_saving

end

end
16 changes: 3 additions & 13 deletions lib/couchrest/model/dirty.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ module Model
# 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
Expand All @@ -21,8 +20,8 @@ module Dirty
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)
Expand All @@ -32,16 +31,7 @@ def couchrest_attribute_will_change!(attr)
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
Expand Down
14 changes: 4 additions & 10 deletions lib/couchrest/model/persistence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def create!
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)
Expand Down Expand Up @@ -143,20 +143,14 @@ def create!(attributes = {})
# 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
Expand Down
37 changes: 11 additions & 26 deletions lib/couchrest/model/properties.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ module Properties

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

Expand All @@ -20,6 +20,12 @@ def properties
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
Expand All @@ -43,31 +49,10 @@ def read_attribute(property)
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.
Expand All @@ -90,7 +75,7 @@ def set_attributes(hash)
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!
Expand Down Expand Up @@ -212,7 +197,7 @@ def define_property(name, options={}, &block)
validates_casted_model property.name
end
properties << property
property_by_name[property.to_s] = property
properties_by_name[property.to_s] = property
property
end

Expand Down
Loading

0 comments on commit 2cc119b

Please sign in to comment.