Refinements to dirty tracking so always enabled unless loading from the database

This commit is contained in:
Sam Lown 2011-04-20 12:31:46 +02:00
commit b3e8fbadc6
13 changed files with 54 additions and 124 deletions

View file

@ -1 +1 @@
1.1.0.beta2 1.1.0.beta3

View file

@ -111,8 +111,8 @@ begin
run_benchmark run_benchmark
end end
set_dirty(false) set_dirty(false)
puts "\nwith use_dirty false"
end end
puts "\nwith use_dirty false"
run_benchmark run_benchmark
end end

View file

@ -1,6 +1,7 @@
== 1.1.0.beta3 == 1.1.0.beta3
* Major changes: * Major changes:
* Fast Dirty Tracking! Many thanks to @sobakasu (Andrew Williams)
* Default CouchRest Model type field now set to 'model' instead of 'couchrest-type'. * Default CouchRest Model type field now set to 'model' instead of 'couchrest-type'.
* Minor enhancements: * Minor enhancements:

View file

@ -40,10 +40,6 @@ module CouchRest
subclasses << subklass subclasses << subklass
end end
# Accessors
attr_accessor :casted_by
# Instantiate a new CouchRest::Model::Base by preparing all properties # Instantiate a new CouchRest::Model::Base by preparing all properties
# using the provided document hash. # using the provided document hash.
# #

View file

@ -11,31 +11,31 @@ module CouchRest::Model
include CouchRest::Model::Associations include CouchRest::Model::Associations
include CouchRest::Model::Validations include CouchRest::Model::Validations
include CouchRest::Model::Dirty include CouchRest::Model::Dirty
attr_accessor :casted_by # attr_accessor :casted_by
end end
def initialize(keys = {}) def initialize(keys = {})
raise StandardError unless self.is_a? Hash raise StandardError unless self.is_a? Hash
prepare_all_attributes(keys) prepare_all_attributes(keys)
super() super()
end end
def []= key, value def []= key, value
couchrest_attribute_will_change!(key) if use_dirty && self[key] != value couchrest_attribute_will_change!(key) if self[key] != value
super(key.to_s, value) super(key.to_s, value)
end end
def [] key def [] key
super(key.to_s) super(key.to_s)
end end
# Gets a reference to the top level extended # Gets a reference to the top level extended
# document that a model is saved inside of # document that a model is saved inside of
def base_doc def base_doc
return nil unless @casted_by return nil unless @casted_by
@casted_by.base_doc @casted_by.base_doc
end end
# False if the casted model has already # False if the casted model has already
# been saved in the containing document # been saved in the containing document
def new? def new?

View file

@ -11,13 +11,11 @@ module CouchRest
add_config :model_type_key add_config :model_type_key
add_config :mass_assign_any_attribute add_config :mass_assign_any_attribute
add_config :auto_update_design_doc add_config :auto_update_design_doc
add_config :use_dirty
configure do |config| configure do |config|
config.model_type_key = 'model' # was 'couchrest-type' config.model_type_key = 'model' # was 'couchrest-type'
config.mass_assign_any_attribute = false config.mass_assign_any_attribute = false
config.auto_update_design_doc = true config.auto_update_design_doc = true
config.use_dirty = true
end end
end end

View file

@ -22,7 +22,7 @@ module CouchRest
def use_dirty? def use_dirty?
bdoc = base_doc bdoc = base_doc
bdoc && bdoc.use_dirty && !bdoc.disable_dirty bdoc && !bdoc.disable_dirty
end end
def couchrest_attribute_will_change!(attr) def couchrest_attribute_will_change!(attr)
@ -30,13 +30,13 @@ module CouchRest
attribute_will_change!(attr) attribute_will_change!(attr)
couchrest_parent_will_change! couchrest_parent_will_change!
end end
def couchrest_parent_will_change! def couchrest_parent_will_change!
@casted_by.couchrest_attribute_will_change!(casted_by_attribute) if @casted_by @casted_by.couchrest_attribute_will_change!(casted_by_attribute) if @casted_by
end end
private private
# return the attribute name this object is referenced by in the parent # return the attribute name this object is referenced by in the parent
def casted_by_attribute def casted_by_attribute
return @casted_by_attribute if @casted_by_attribute return @casted_by_attribute if @casted_by_attribute

View file

@ -30,7 +30,7 @@ module CouchRest
def update(options = {}) def update(options = {})
raise "Calling #{self.class.name}#update on document that has not been created!" if self.new? raise "Calling #{self.class.name}#update on document that has not been created!" if self.new?
return false unless perform_validations(options) return false unless perform_validations(options)
return true if use_dirty? && !self.changed? return true if !self.changed?
_run_update_callbacks do _run_update_callbacks do
_run_save_callbacks do _run_save_callbacks do
result = database.save_doc(self) result = database.save_doc(self)

View file

@ -6,9 +6,9 @@ module CouchRest
included do included do
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties) extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
extlib_inheritable_accessor(:prop_by_name) unless self.respond_to?(:prop_by_name) extlib_inheritable_accessor(:property_by_name) unless self.respond_to?(:property_by_name)
self.properties ||= [] self.properties ||= []
self.prop_by_name ||= {} self.property_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?(:[]=)) 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 end
@ -39,19 +39,12 @@ module CouchRest
end end
# Store a casted value in the current instance of an attribute defined # Store a casted value in the current instance of an attribute defined
# with a property. # with a property and update dirty status
def write_attribute(property, value) def write_attribute(property, value)
prop = find_property!(property)
self[prop.to_s] = prop.is_a?(String) ? value : prop.cast(self, value)
end
# write property, update dirty status
def write_attribute_dirty(property, value)
prop = find_property!(property) prop = find_property!(property)
value = prop.is_a?(String) ? value : prop.cast(self, value) value = prop.is_a?(String) ? value : prop.cast(self, value)
propname = prop.to_s attribute_will_change!(prop.name) if use_dirty? && self[prop.name] != value
attribute_will_change!(propname) if use_dirty? && self[propname] != value self[prop.name] = value
self[propname] = value
end end
def []=(key,value) def []=(key,value)
@ -82,34 +75,47 @@ module CouchRest
# Remove any protected and update all the rest. Any attributes # Remove any protected and update all the rest. Any attributes
# which do not have a property will simply be ignored. # which do not have a property will simply be ignored.
attrs = remove_protected_attributes(hash) attrs = remove_protected_attributes(hash)
directly_set_attributes(attrs, :dirty => true) directly_set_attributes(attrs)
end end
alias :attributes= :update_attributes_without_saving alias :attributes= :update_attributes_without_saving
# 'attributes' needed for Dirty # 'attributes' needed for Dirty
alias :attributes :properties_with_values alias :attributes :properties_with_values
def set_attributes(hash)
attrs = remove_protected_attributes(hash)
directly_set_attributes(attrs)
end
protected
def find_property(property) def find_property(property)
property.is_a?(Property) ? property : self.class.prop_by_name[property.to_s] property.is_a?(Property) ? property : self.class.property_by_name[property.to_s]
end end
# The following methods should be accessable by the Model::Base Class, but not by anything else! # The following methods should be accessable by the Model::Base Class, but not by anything else!
def apply_all_property_defaults def apply_all_property_defaults
return if self.respond_to?(:new?) && (new? == false) return if self.respond_to?(:new?) && (new? == false)
# TODO: cache the default object # TODO: cache the default object
# Never mark default options as dirty!
dirty, self.disable_dirty = self.disable_dirty, true
self.class.properties.each do |property| self.class.properties.each do |property|
write_attribute(property, property.default_value) write_attribute(property, property.default_value)
end end
self.disable_dirty = dirty
end end
def prepare_all_attributes(doc = {}, options = {}) def prepare_all_attributes(doc = {}, options = {})
self.disable_dirty = !!options[:directly_set_attributes]
apply_all_property_defaults apply_all_property_defaults
if options[:directly_set_attributes] if options[:directly_set_attributes]
directly_set_read_only_attributes(doc) directly_set_read_only_attributes(doc)
else else
doc = remove_protected_attributes(doc) doc = remove_protected_attributes(doc)
end end
directly_set_attributes(doc) unless doc.nil? res = doc.nil? ? doc : directly_set_attributes(doc)
self.disable_dirty = false
res
end end
def find_property!(property) def find_property!(property)
@ -120,9 +126,8 @@ module CouchRest
# Set all the attributes and return a hash with the attributes # Set all the attributes and return a hash with the attributes
# that have not been accepted. # that have not been accepted.
def directly_set_attributes(hash, options = {}) def directly_set_attributes(hash)
self.disable_dirty = !options[:dirty] hash.reject do |attribute_name, attribute_value|
ret = hash.reject do |attribute_name, attribute_value|
if self.respond_to?("#{attribute_name}=") if self.respond_to?("#{attribute_name}=")
self.send("#{attribute_name}=", attribute_value) self.send("#{attribute_name}=", attribute_value)
true true
@ -133,8 +138,6 @@ module CouchRest
false false
end end
end end
self.disable_dirty = false
ret
end end
def directly_set_read_only_attributes(hash) def directly_set_read_only_attributes(hash)
@ -147,10 +150,6 @@ module CouchRest
end end
end end
def set_attributes(hash)
attrs = remove_protected_attributes(hash)
directly_set_attributes(attrs)
end
module ClassMethods module ClassMethods
@ -206,14 +205,14 @@ module CouchRest
end end
type = [type] # inject as an array type = [type] # inject as an array
end end
property = Property.new(name, type, options.merge(:use_dirty => use_dirty)) property = Property.new(name, type, options)
create_property_getter(property) create_property_getter(property)
create_property_setter(property) unless property.read_only == true create_property_setter(property) unless property.read_only == true
if property.type_class.respond_to?(:validates_casted_model) if property.type_class.respond_to?(:validates_casted_model)
validates_casted_model property.name validates_casted_model property.name
end end
properties << property properties << property
prop_by_name[property.to_s] = property property_by_name[property.to_s] = property
property property
end end
@ -247,7 +246,7 @@ module CouchRest
property_name = property.name property_name = property.name
class_eval <<-EOS class_eval <<-EOS
def #{property_name}=(value) def #{property_name}=(value)
write_attribute_dirty('#{property_name}', value) write_attribute('#{property_name}', value)
end end
EOS EOS

View file

@ -4,7 +4,7 @@ module CouchRest::Model
include ::CouchRest::Model::Typecast include ::CouchRest::Model::Typecast
attr_reader :name, :type, :type_class, :read_only, :alias, :default, :casted, :init_method, :use_dirty, :options attr_reader :name, :type, :type_class, :read_only, :alias, :default, :casted, :init_method, :options
# Attribute to define. # Attribute to define.
# All Properties are assumed casted unless the type is nil. # All Properties are assumed casted unless the type is nil.
@ -38,8 +38,8 @@ module CouchRest::Model
end end
arr = value.collect { |data| cast_value(parent, data) } arr = value.collect { |data| cast_value(parent, data) }
# allow casted_by calls to be passed up chain by wrapping in CastedArray # allow casted_by calls to be passed up chain by wrapping in CastedArray
value = (use_dirty || type_class != String) ? CastedArray.new(arr, self) : arr value = CastedArray.new(arr, self)
value.casted_by = parent if value.respond_to?(:casted_by) value.casted_by = parent
elsif (type == Object || type == Hash) && (value.class == Hash) elsif (type == Object || type == Hash) && (value.class == Hash)
# allow casted_by calls to be passed up chain by wrapping in CastedHash # allow casted_by calls to be passed up chain by wrapping in CastedHash
value = CouchRest::Model::CastedHash[value] value = CouchRest::Model::CastedHash[value]
@ -94,7 +94,6 @@ module CouchRest::Model
@alias = options.delete(:alias) if options[:alias] @alias = options.delete(:alias) if options[:alias]
@default = options.delete(:default) unless options[:default].nil? @default = options.delete(:default) unless options[:default].nil?
@init_method = options[:init_method] ? options.delete(:init_method) : 'new' @init_method = options[:init_method] ? options.delete(:init_method) : 'new'
@use_dirty = options.delete(:use_dirty)
@options = options @options = options
end end

View file

@ -14,88 +14,25 @@ class WithCastedModelMixin < Hash
end end
class DummyModel < CouchRest::Model::Base class DummyModel < CouchRest::Model::Base
use_database TEST_SERVER.default_database use_database DB
raise "Default DB not set" if TEST_SERVER.default_database.nil?
property :casted_attribute, WithCastedModelMixin property :casted_attribute, WithCastedModelMixin
property :title, :default => 'Sample Title'
property :details, Object, :default => { 'color' => 'blue' } property :details, Object, :default => { 'color' => 'blue' }
property :keywords, [String], :default => ['default-keyword'] property :keywords, [String], :default => ['default-keyword']
property :sub_models do |child| property :sub_models do
child.property :title property :title
end end
end end
# set dirty configuration, return previous configuration setting describe "Dirty" do
def set_dirty(value)
orig = nil
CouchRest::Model::Base.configure do |config|
orig = config.use_dirty
config.use_dirty = value
end
Card.instance_eval do
self.use_dirty = value
end
orig
end
describe "With use_dirty(off)" do
before(:all) do
@use_dirty_orig = set_dirty(false)
end
# turn dirty back to default
after(:all) do
set_dirty(@use_dirty_orig)
end
describe "changes" do
it "should not respond to the changes method" do
@card = Card.new
@card.first_name = "andrew"
@card.changes.should == {}
end
end
describe "changed?" do
it "should not record changes" do
@card = Card.new
@card.first_name = "andrew"
@card.changed?.should be_false
end
end
describe "save" do
it "should save unchanged records" do
@card = Card.create!(:first_name => "matt")
@card = Card.find(@card.id)
@card.database.should_receive(:save_doc).and_return({"ok" => true})
@card.save
end
end
end
describe "With use_dirty(on)" do
before(:all) do
@use_dirty_orig = set_dirty(true)
end
# turn dirty back to default
after(:all) do
set_dirty(@use_dirty_orig)
end
describe "changes" do describe "changes" do
it "should return changes on an attribute" do it "should return changes on an attribute" do
@card = Card.new(:first_name => "matt") @card = Card.new(:first_name => "matt")
@card.first_name = "andrew" @card.first_name = "andrew"
@card.first_name_changed?.should be_true
@card.changes.should == { "first_name" => ["matt", "andrew"] } @card.changes.should == { "first_name" => ["matt", "andrew"] }
end end

View file

@ -357,10 +357,10 @@ describe "Property Class" do
property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should eql(CouchRest::Model::CastedArray) property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should eql(CouchRest::Model::CastedArray)
end end
it "should not set a CastedArray on array of Strings" do it "should set a CastedArray on array of Strings" do
property = CouchRest::Model::Property.new(:test, [String]) property = CouchRest::Model::Property.new(:test, [String])
parent = mock("FooObject") parent = mock("FooObject")
property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should_not eql(CouchRest::Model::CastedArray) property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should eql(CouchRest::Model::CastedArray)
end end
it "should raise and error if value is array when type is not" do it "should raise and error if value is array when type is not" do

View file

@ -7,10 +7,10 @@ class Card < CouchRest::Model::Base
property :last_name, :alias => :family_name property :last_name, :alias => :family_name
property :read_only_value, :read_only => true property :read_only_value, :read_only => true
property :cast_alias, Person, :alias => :calias property :cast_alias, Person, :alias => :calias
property :fg_color, :default => '#000'
timestamps! timestamps!
# Validation # Validation
validates_presence_of :first_name validates_presence_of :first_name