Refinements to dirty tracking so always enabled unless loading from the database
This commit is contained in:
commit
b3e8fbadc6
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.
|
||||||
#
|
#
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
4
spec/fixtures/more/card.rb
vendored
4
spec/fixtures/more/card.rb
vendored
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue