Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added partial updating. First pass…

  • Loading branch information...
commit 2d1a9664790a0f73aa875ee58295837087d2bef5 1 parent 34b6edf
@jnunemaker authored
View
4 Gemfile
@@ -4,8 +4,8 @@ gemspec
gem 'rake', '~> 0.9.0'
# keep mongo and bson ext at same version
-gem 'adapter-mongo', '~> 0.5'
-gem 'mongo', '~> 1.6.0'
+gem 'adapter-mongo', '~> 0.5.5'
+gem 'mongo', '~> 1.6.0'
gem 'bson_ext', '~> 1.6.0', :require => false
group(:guard) do
View
2  lib/toy/mongo.rb
@@ -3,6 +3,7 @@
require 'toy/extensions/bson_object_id'
require 'toy/identity/object_id_key_factory'
require 'toy/mongo/querying'
+require 'toy/mongo/partial_updating'
require 'adapter/mongo'
module Toy
@@ -12,6 +13,7 @@ module Mongo
included do
include Toy::Store
include Querying
+ include PartialUpdating
key Toy::Identity::ObjectIdKeyFactory.new
end
View
54 lib/toy/mongo/partial_updating.rb
@@ -0,0 +1,54 @@
+module Toy
+ module Mongo
+ module PartialUpdating
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :partial_updates
+
+ self.partial_updates = false
+ end
+
+ # Very basic method for determining what has changed locally
+ # so we can just update changes instead of entire document
+ #
+ # Does not work with complex objects (array, hash, set, etc.)
+ # as it does not attempt to determine what has changed in them,
+ # just whether or not they have changed at all.
+ def persistable_changes
+ attrs = {}
+ pattrs = persisted_attributes
+ changed.each do |key|
+ attribute = self.class.attributes[key.to_s]
+ next if attribute.virtual?
+ attrs[attribute.persisted_name] = pattrs[attribute.persisted_name]
+ end
+ attrs
+ end
+
+ def persist
+ if partial_updates?
+ updates = persistable_changes
+ if new_record? || (persisted? && updates.present?)
+ adapter.write id, updates
+ end
+ else
+ super
+ end
+ end
+
+ def atomic_update(update, opts={})
+ options = {}
+ criteria = {'_id' => id}
+ criteria.update(opts[:criteria]) if opts[:criteria]
+ options[:safe] = opts.key?(:safe) ? opts[:safe] : adapter.options[:safe]
+
+ run_callbacks(:save) do
+ run_callbacks(:update) do
+ adapter.client.update(criteria, update, options)
+ end
+ end
+ end
+ end
+ end
+end
View
30 lib/toy/mongo/querying.rb
@@ -51,36 +51,6 @@ def query
end
end
end
-
- # Very basic method for determining what has changed locally
- # so we can just update changes instead of entire document
- #
- # Does not work with complex objects (array, hash, set, etc.)
- # as it does not attempt to determine what has changed in them,
- # just whether or not they have changed at all.
- def persistable_changes
- attrs = {}
- pattrs = persisted_attributes
- changed.each do |key|
- attribute = self.class.attributes[key.to_s]
- next if attribute.virtual?
- attrs[attribute.persisted_name] = pattrs[attribute.persisted_name]
- end
- attrs
- end
-
- def atomic_update(update, opts={})
- options = {}
- criteria = {'_id' => id}
- criteria.update(opts[:criteria]) if opts[:criteria]
- options[:safe] = opts.key?(:safe) ? opts[:safe] : adapter.options[:safe]
-
- run_callbacks(:save) do
- run_callbacks(:update) do
- adapter.client.update(criteria, update, options)
- end
- end
- end
end
end
end
View
198 spec/toy/mongo/partial_updating_spec.rb
@@ -0,0 +1,198 @@
+require 'helper'
+
+describe Toy::Mongo::PartialUpdating do
+ uses_constants 'User'
+
+ before(:each) do
+ User.send :include, CallbacksHelper
+ User.attribute :name, String
+ User.attribute :bio, String
+ User.attribute :password, String, :virtual => true
+ User.attribute :email, String, :abbr => :e
+ end
+
+ describe "#persistable_changes" do
+ before(:each) do
+ User.attribute(:password, String, :virtual => true)
+ User.attribute(:email, String, :abbr => :e)
+ @user = User.create(:name => 'John', :password => 'secret', :email => 'nunemaker@gmail.com')
+ end
+
+ it "returns only changed attributes" do
+ @user.name = 'Frank'
+ @user.persistable_changes.should == {'name' => 'Frank'}
+ end
+
+ it "returns typecast values" do
+ @user.name = 1234
+ @user.persistable_changes.should == {'name' => '1234'}
+ end
+
+ it "ignores virtual attributes" do
+ @user.password = 'ignore me'
+ @user.persistable_changes.should be_empty
+ end
+
+ it "uses abbreviated key" do
+ @user.email = 'john@orderedlist.com'
+ @user.persistable_changes.should == {'e' => 'john@orderedlist.com'}
+ end
+ end
+
+ describe "#atomic_update" do
+ before(:each) do
+ @user = User.create(:name => 'John')
+ end
+
+ it "performs update" do
+ @user.atomic_update('$set' => {'name' => 'Frank'})
+ @user.reload
+ @user.name.should == 'Frank'
+ end
+
+ it "defaults to adapter's :safe option" do
+ @user.adapter.client.should_receive(:update).with(kind_of(Hash), kind_of(Hash), :safe => nil)
+ @user.atomic_update('$set' => {'name' => 'Frank'})
+
+ User.adapter(:mongo, STORE, :safe => false)
+ @user.adapter.client.should_receive(:update).with(kind_of(Hash), kind_of(Hash), :safe => false)
+ @user.atomic_update('$set' => {'name' => 'Frank'})
+
+ User.adapter(:mongo, STORE, :safe => true)
+ @user.adapter.client.should_receive(:update).with(kind_of(Hash), kind_of(Hash), :safe => true)
+ @user.atomic_update('$set' => {'name' => 'Frank'})
+ end
+
+ it "runs callbacks in correct order" do
+ doc = User.create.tap(&:clear_history)
+ doc.atomic_update({})
+ doc.history.should == [:before_save, :before_update, :after_update, :after_save]
+ end
+
+ context "with :safe option" do
+ it "overrides adapter's :safe option" do
+ User.adapter(:mongo, STORE, :safe => false)
+ @user.adapter.client.should_receive(:update).with(kind_of(Hash), kind_of(Hash), :safe => true)
+ @user.atomic_update({'$set' => {'name' => 'Frank'}}, :safe => true)
+
+ User.adapter(:mongo, STORE, :safe => true)
+ @user.adapter.client.should_receive(:update).with(kind_of(Hash), kind_of(Hash), :safe => false)
+ @user.atomic_update({'$set' => {'name' => 'Frank'}}, :safe => false)
+ end
+ end
+
+ context "with :criteria option" do
+ uses_constants('Site')
+
+ it "allows updating embedded documents using $ positional operator" do
+ User.attribute(:sites, Array)
+ site1 = Site.create
+ site2 = Site.create
+ @user.update_attributes(:sites => [{'id' => site1.id, 'ui' => 1}, {'id' => site2.id, 'ui' => 2}])
+
+ @user.atomic_update(
+ {'$set' => {'sites.$.ui' => 2}},
+ {:criteria => {'sites.id' => site1.id}}
+ )
+ @user.reload
+ @user.sites.map { |s| s['ui'] }.should == [2, 2]
+ end
+ end
+ end
+
+ describe ".partial_updates" do
+ it "defaults to false" do
+ User.partial_updates.should be_false
+ end
+
+ it "can be turned on" do
+ User.partial_updates = true
+ User.partial_updates.should be_true
+ end
+
+ it "is inherited" do
+ User.partial_updates = true
+ subclass = Class.new(User)
+ subclass.partial_updates.should be_true
+ end
+
+ it "is inherited separate from superclass" do
+ User.partial_updates = true
+ subclass = Class.new(User)
+ subclass.partial_updates = false
+ User.partial_updates.should be_true
+ subclass.partial_updates.should be_false
+ end
+ end
+
+ describe "#persist" do
+ context "with partial updates on" do
+ before do
+ User.adapter :mongo_atomic, STORE
+ User.partial_updates = true
+ end
+
+ it "only persists changes" do
+ user = User.create(:name => 'John', :bio => 'Awesome.')
+ user.name = 'Johnny'
+
+ # simulate outside change
+ user.adapter.client.update({:_id => user.id}, {'$set' => {:bio => 'Surprise!'}})
+
+ user.save
+ user.reload
+
+ user.name.should eq('Johnny')
+ user.bio.should eq('Surprise!')
+ end
+
+ it "does not persist virtual attributes" do
+ user = User.new(:name => 'John')
+ user.password = 'hacks'
+ user.adapter.should_receive(:write).with(user.id, {
+ 'name' => 'John',
+ })
+ user.save
+ end
+
+ it "does persist new records even without changes" do
+ user = User.create
+ user.persisted?.should be_true
+ end
+
+ it "does not persist if there were no changes" do
+ user = User.create
+ user.adapter.should_not_receive(:write)
+ user.save
+ end
+
+ it "works with abbreviated attributes" do
+ user = User.new(:email => 'john@doe.com')
+ user.adapter.should_receive(:write).with(user.id, {
+ 'e' => 'john@doe.com',
+ })
+ user.save
+ end
+ end
+
+ context "with partial updates off" do
+ before do
+ User.partial_updates = false
+ end
+
+ it "persists entire document blowing away outside changes" do
+ user = User.create(:name => 'John', :bio => 'Awesome.')
+ user.name = 'Johnny'
+
+ # simulate outside change
+ user.adapter.client.update({:_id => user.id}, {'$set' => {:bio => 'Surprise!'}})
+
+ user.save
+ user.reload
+
+ user.name.should eq('Johnny')
+ user.bio.should eq('Awesome.')
+ end
+ end
+ end
+end
View
96 spec/toy/mongo/querying_spec.rb
@@ -1,12 +1,11 @@
require 'helper'
describe Toy::Mongo::Querying do
- uses_constants('User')
+ uses_constants 'User'
before(:each) do
- User.send(:include, CallbacksHelper)
- User.attribute(:name, String)
- User.attribute(:bio, String)
+ User.attribute :name, String
+ User.attribute :bio, String
end
describe "#query" do
@@ -24,95 +23,6 @@
end
end
- describe "#atomic_update" do
- before(:each) do
- @user = User.create(:name => 'John')
- end
-
- it "performs update" do
- @user.atomic_update('$set' => {'name' => 'Frank'})
- @user.reload
- @user.name.should == 'Frank'
- end
-
- it "defaults to adapter's :safe option" do
- @user.adapter.client.should_receive(:update).with(kind_of(Hash), kind_of(Hash), :safe => nil)
- @user.atomic_update('$set' => {'name' => 'Frank'})
-
- User.adapter(:mongo, STORE, :safe => false)
- @user.adapter.client.should_receive(:update).with(kind_of(Hash), kind_of(Hash), :safe => false)
- @user.atomic_update('$set' => {'name' => 'Frank'})
-
- User.adapter(:mongo, STORE, :safe => true)
- @user.adapter.client.should_receive(:update).with(kind_of(Hash), kind_of(Hash), :safe => true)
- @user.atomic_update('$set' => {'name' => 'Frank'})
- end
-
- it "runs callbacks in correct order" do
- doc = User.create.tap(&:clear_history)
- doc.atomic_update({})
- doc.history.should == [:before_save, :before_update, :after_update, :after_save]
- end
-
- context "with :safe option" do
- it "overrides adapter's :safe option" do
- User.adapter(:mongo, STORE, :safe => false)
- @user.adapter.client.should_receive(:update).with(kind_of(Hash), kind_of(Hash), :safe => true)
- @user.atomic_update({'$set' => {'name' => 'Frank'}}, :safe => true)
-
- User.adapter(:mongo, STORE, :safe => true)
- @user.adapter.client.should_receive(:update).with(kind_of(Hash), kind_of(Hash), :safe => false)
- @user.atomic_update({'$set' => {'name' => 'Frank'}}, :safe => false)
- end
- end
-
- context "with :criteria option" do
- uses_constants('Site')
-
- it "allows updating embedded documents using $ positional operator" do
- User.attribute(:sites, Array)
- site1 = Site.create
- site2 = Site.create
- @user.update_attributes(:sites => [{'id' => site1.id, 'ui' => 1}, {'id' => site2.id, 'ui' => 2}])
-
- @user.atomic_update(
- {'$set' => {'sites.$.ui' => 2}},
- {:criteria => {'sites.id' => site1.id}}
- )
- @user.reload
- @user.sites.map { |s| s['ui'] }.should == [2, 2]
- end
- end
- end
-
- describe "#persistable_changes" do
- before(:each) do
- User.attribute(:password, String, :virtual => true)
- User.attribute(:email, String, :abbr => :e)
- @user = User.create(:name => 'John', :password => 'secret', :email => 'nunemaker@gmail.com')
- end
-
- it "returns only changed attributes" do
- @user.name = 'Frank'
- @user.persistable_changes.should == {'name' => 'Frank'}
- end
-
- it "returns typecast values" do
- @user.name = 1234
- @user.persistable_changes.should == {'name' => '1234'}
- end
-
- it "ignores virtual attributes" do
- @user.password = 'ignore me'
- @user.persistable_changes.should be_empty
- end
-
- it "uses abbreviated key" do
- @user.email = 'john@orderedlist.com'
- @user.persistable_changes.should == {'e' => 'john@orderedlist.com'}
- end
- end
-
describe "#get" do
before(:each) do
@user = User.create
Please sign in to comment.
Something went wrong with that request. Please try again.