Skip to content

Commit

Permalink
Added partial updating. First pass…
Browse files Browse the repository at this point in the history
  • Loading branch information
jnunemaker committed May 20, 2012
1 parent 34b6edf commit 2d1a966
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 125 deletions.
4 changes: 2 additions & 2 deletions Gemfile
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/toy/mongo.rb
Expand Up @@ -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
Expand All @@ -12,6 +13,7 @@ module Mongo
included do
include Toy::Store
include Querying
include PartialUpdating

key Toy::Identity::ObjectIdKeyFactory.new
end
Expand Down
54 changes: 54 additions & 0 deletions 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
30 changes: 0 additions & 30 deletions lib/toy/mongo/querying.rb
Expand Up @@ -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
198 changes: 198 additions & 0 deletions 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

0 comments on commit 2d1a966

Please sign in to comment.