mm_partial_update 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -14,7 +14,11 @@ To activate the plugin, add 'mm_partial_update' to your gemfile
14
14
 
15
15
  == Usage
16
16
 
17
- mm-partial-update does not, by default, change MongoMappers normal mode of operation. Although mm-partial-update can be configured, either at a global level or on a model by model basis, to use a partial update strategy for all persistence operations, out of the box it simply adds a "save_changes" method to both MongoMapper::Document and MongoMapper::EmbeddedDocument, which will persist, using MongoDB's atomic operators, any changes to the target document or embedded document as well as any descendents of the target document or embedded document.
17
+ mm-partial-update does not, by default, change MongoMappers normal mode of operation. Although mm-partial-update can be configured, either at a global level or on a model by model basis, to use a partial update strategy for all persistence operations, out of the box it simply adds #save_changes and #save_changes! methods to both MongoMapper::Document and MongoMapper::EmbeddedDocument. #save_changes(!) will persist any changes to the target document using MongoDB's atomic operators, ideal for making small changes to large documents or for concurrent modification of documents by multiple processes.
18
+
19
+ As you might expect, #save_changes and #save_changes! mirror the behavior of #save and #save!, in that #save_changes will return false if the document (or embedded document) having its changes saved fails validation, whereas #save_changes! will raise a MongoMapper::InvalidDocument error.
20
+
21
+ Both methods behave identically to their native MongoMapper counterparts with respect to both validations and callbacks.
18
22
 
19
23
  === For example:
20
24
 
@@ -35,11 +39,25 @@ mm-partial-update does not, by default, change MongoMappers normal mode of opera
35
39
 
36
40
  person = Person.create! :name=>"Willard"
37
41
  person.name = "Poe"
38
- person.save_changes #only saves the changed fields (in this case name=>"Poe"
42
+ person.save_changes! #only saves the changed fields (in this case name=>"Poe"
43
+
44
+ You can also persist only a part of a document:
45
+
46
+ person = Person.create! :name=>"Willard"
47
+ person.name = "Benji"
48
+ pet = person.pets.build :name=>"Magma"
49
+
50
+ person.changed? #= > true
51
+ pet.changed? # => true
52
+
53
+ pet.save_changes
54
+
55
+ pet.changed? # => false
56
+ person.changed? # => true
39
57
 
40
- However, in addition to #save_changes, partial saves can be enabled globally:
58
+ In addition to #save_changes(!), partial saves can be enabled globally:
41
59
 
42
- MmPartialUpdate.persistence_strategy = :changes_only
60
+ MmPartialUpdate.default_persistence_strategy = :changes_only
43
61
 
44
62
  Or on a model by model basis:
45
63
 
@@ -50,7 +68,7 @@ Or on a model by model basis:
50
68
  many :pets
51
69
  end
52
70
 
53
- When enabled globally, all calls to save or save! will result in a partial update (with the exception of new documents). When enabled on a particular model, calls to save or save! on instances of that model and any subclasses of that model will result in partial updates. This is useful if you have a number of models but only one or two require partial saves for a particular use case, such as concurrent modification by more than one process.
71
+ When enabled globally, all calls to #save or #save! across all models will simply delegate to #save_changes or #save_changes!, resulting in a partial update. When enabled on a particular model, calls to save or save! on instances of that model and any subclasses of that model will result in partial updates.
54
72
 
55
73
  == Known Issues & Limitations
56
74
 
@@ -10,11 +10,15 @@ module MmPartialUpdate
10
10
 
11
11
  module InstanceMethods
12
12
 
13
+ def create_or_update_changes(options={})
14
+ new? ? save(:validate=>false, :safe=>options[:safe]) : super
15
+ end
16
+
13
17
  def save_to_collection(options={})
14
18
  strategy = determine_persistence_strategy(options)
15
19
  return super if new? || strategy == :full_document
16
20
 
17
- save_changes(options)
21
+ save_changes(options.merge(:validate=>false, :callbacks=>false))
18
22
  end
19
23
 
20
24
  private
@@ -10,6 +10,11 @@ module MmPartialUpdate
10
10
 
11
11
  module InstanceMethods
12
12
 
13
+ def create_or_update_changes(options={})
14
+ assert_root_saved
15
+ super
16
+ end
17
+
13
18
  def database_selector
14
19
  selector = @_association_name.to_s
15
20
  selector = "#{_parent_document.database_selector}.#{selector}" if
@@ -34,6 +39,10 @@ module MmPartialUpdate
34
39
  end
35
40
  end
36
41
 
42
+ def assert_root_saved
43
+ raise "You are attempting to save changes to an embedded document, but the root document has not yet been saved. You must save changes to the root document before you can call save_changes any of its embedded documents" if _root_document.new?
44
+ end
45
+
37
46
  end
38
47
 
39
48
  end
@@ -1,4 +1,5 @@
1
- # encoding: UTF-8
1
+ require "mongo_mapper/exceptions.rb"
2
+
2
3
  module MmPartialUpdate
3
4
  module Plugins
4
5
  module PartialUpdate
@@ -27,25 +28,21 @@ module MmPartialUpdate
27
28
  module InstanceMethods
28
29
 
29
30
  def save_changes(options={})
30
- #We can't update an embedded document if the root isn't saved
31
- #The clear_changes call is added here because dirty
32
- #tracking happens further up the call chain than save_to_collection
33
- #under normal circumstances, so we have to inject it
34
- return _root_document.save_to_collection(options).tap {clear_changes} if
35
- _root_document.new?
36
-
37
- #persist changes to self and descendents
38
- update_command = prepare_update_command
39
- update_command.execute()
31
+ options.assert_valid_keys(:validate, :callbacks, :safe, :changes_only)
32
+ options.reverse_merge!(:validate=>true, :callbacks=>true)
33
+ !options[:validate] || valid? ? create_or_update_changes(options) : false
34
+ end
40
35
 
41
- #clear dirty tracking
42
- @_new = false
43
- clear_changes
44
- associations.each do |_, association|
45
- proxy = get_proxy(association)
46
- proxy.save_to_collection(options) if
47
- proxy.proxy_respond_to?(:save_to_collection)
48
- end
36
+ def save_changes!(options={})
37
+ options.assert_valid_keys(:callbacks, :safe, :changes_only)
38
+ save_changes(options) || raise(MongoMapper::DocumentNotValid.new(self))
39
+ end
40
+
41
+ def create_or_update_changes(options={})
42
+ #assert_root_saved
43
+ update_command = prepare_update_command
44
+ execute_command(update_command, options)
45
+ clear_changes_to_subtree(options)
49
46
  end
50
47
 
51
48
  def prepare_update_command
@@ -86,6 +83,29 @@ module MmPartialUpdate
86
83
  proxy
87
84
  end
88
85
 
86
+ def clear_changes_to_subtree(options)
87
+ @_new = false
88
+ clear_changes
89
+ associations.each do |_, association|
90
+ proxy = get_proxy(association)
91
+ proxy.save_to_collection(options) if
92
+ proxy.proxy_respond_to?(:save_to_collection)
93
+ end
94
+ end
95
+
96
+ def execute_command(update_command,options)
97
+ if options[:callbacks]
98
+ context = new? ? :create : :update
99
+ run_callbacks(:save) do
100
+ run_callbacks(context) do
101
+ update_command.execute()
102
+ end
103
+ end
104
+ else
105
+ update_command.execute()
106
+ end
107
+ end
108
+
89
109
  end
90
110
 
91
111
  end
@@ -1,4 +1,4 @@
1
1
  # encoding: UTF-8
2
2
  module MmPartialUpdate
3
- Version = '0.1.0'
3
+ Version = '0.1.1'
4
4
  end
@@ -0,0 +1,27 @@
1
+ #from MongoMapper test suite
2
+ module CallbacksSupport
3
+ def self.included base
4
+ base.key :name, String
5
+
6
+ [ :after_find, :after_initialize,
7
+ :before_validation, :after_validation,
8
+ :before_create, :after_create,
9
+ :before_update, :after_update,
10
+ :before_save, :after_save,
11
+ :before_destroy, :after_destroy
12
+ ].each do |callback|
13
+ base.send(callback) do
14
+ history << callback.to_sym
15
+ end
16
+ end
17
+ end
18
+
19
+ def history
20
+ @history ||= []
21
+ end
22
+
23
+ def clear_history
24
+ embedded_associations.each { |a| self.send(a.name).each(&:clear_history) }
25
+ @history = nil
26
+ end
27
+ end
@@ -1,6 +1,7 @@
1
1
  require 'test_helper'
2
2
  require "models"
3
3
 
4
+
4
5
  class TestEmbeddedDocumentPlugin < Test::Unit::TestCase
5
6
 
6
7
  context "#database_selector" do
@@ -73,15 +74,10 @@ class TestEmbeddedDocumentPlugin < Test::Unit::TestCase
73
74
 
74
75
  context "#save_changes" do
75
76
 
76
- should "save an unsaved parent when a descendent is saved" do
77
+ should "fails when a descendent is saved with an unsaved root" do
77
78
  person = Person.new :name=>"Willard"
78
79
  pet = person.pets.build :name=>"Magma"
79
- pet.save_changes
80
- person = Person.find(person.id)
81
- person.should_not be_nil
82
- person.name.should == "Willard"
83
- person.pets.count.should == 1
84
- person.pets[0].name.should == "Magma"
80
+ assert_raises(RuntimeError) { pet.save_changes }
85
81
  end
86
82
 
87
83
  should "create an embedded document that doesn't already exist" do
@@ -91,11 +91,73 @@ class TestPartialUpdate < Test::Unit::TestCase
91
91
  end
92
92
  end
93
93
 
94
- #context "when called on embedded documents" do
95
- # should "save a new embedded document" do
96
- # person = Person.create! :name=>"Willard"
97
- # end
98
- #end
94
+ end
95
+
96
+ context "#save_changes!" do
97
+
98
+ should "raise an exception if the document isn't valid" do
99
+ person = ValidatedPerson.new
100
+ assert_raise(MongoMapper::DocumentNotValid) { person.save_changes! }
101
+ end
102
+
103
+ should "not raise an exception if the document is valid" do
104
+ person = ValidatedPerson.new :name=>"Willard"
105
+ assert_nothing_thrown { person.save_changes! }
106
+ end
107
+
108
+ should "raise an exception when saving an embedded document that isn't valid" do
109
+ person = ValidatedPerson.create! :name=>"Willard"
110
+ pet = person.validated_pets.build
111
+ assert_raise(MongoMapper::DocumentNotValid) {pet.save_changes!}
112
+ end
113
+
114
+ should "raise an exception when saving an embedded 'one' document that isn't valid" do
115
+ person = ValidatedPerson.create! :name=>"Willard"
116
+ pet = person.validated_pet.build
117
+ assert_raise(MongoMapper::DocumentNotValid) {pet.save_changes!}
118
+ end
119
+
120
+ end
121
+
122
+ context "callbacks" do
123
+
124
+ should "happen for new top level documents" do
125
+ p = Person.new :name=>"Willard"
126
+ p.clear_history
127
+ p.save_changes
128
+ p.history.should == [:before_validation, :after_validation,
129
+ :before_save, :before_create, :after_create, :after_save]
130
+ end
131
+
132
+ should "happen for updated top level documents" do
133
+ p = Person.create :name=>"Willard"
134
+ p.clear_history
135
+ p.name = "Timmy"
136
+ p.save_changes
137
+ p.history.should == [:before_validation, :after_validation,
138
+ :before_save, :before_update, :after_update, :after_save]
139
+ end
140
+
141
+ should "happen for new embedded documents" do
142
+ p = Person.create :name=>"Willard"
143
+ pet = p.pets.build :name=>"Magma"
144
+ pet.clear_history
145
+ pet.save_changes
146
+ pet.history.should == [:before_validation, :after_validation,
147
+ :before_save, :before_create, :after_create, :after_save]
148
+ end
149
+
150
+ should "happen for updated embedded documents" do
151
+ p = Person.new :name=>"Willard"
152
+ pet = p.pets.build :name=>"Magma"
153
+ p.save!
154
+ pet.clear_history
155
+ pet.name = "Debris"
156
+ pet.save_changes
157
+ pet.history.should == [:before_validation, :after_validation,
158
+ :before_save, :before_update, :after_update, :after_save]
159
+ end
99
160
 
100
161
  end
162
+
101
163
  end
data/test/models.rb CHANGED
@@ -1,18 +1,35 @@
1
+ require "callbacks_support"
2
+
1
3
  class Person
2
4
  include MongoMapper::Document
5
+ include CallbacksSupport
6
+
3
7
  key :name, String
4
8
  many :pets
5
9
  one :favorite_pet, :class_name=>'Pet'
10
+
11
+
12
+ end
13
+
14
+ class ValidatedPerson < Person
15
+ validates_presence_of :name
16
+ one :validated_pet
17
+ many :validated_pets
6
18
  end
7
19
 
8
20
  class Pet
9
21
  include MongoMapper::EmbeddedDocument
22
+ include CallbacksSupport
10
23
  key :name, String
11
24
  key :age, Integer
12
25
  many :fleas
13
26
  one :favorite_flea, :class_name=>'Flea'
14
27
  end
15
28
 
29
+ class ValidatedPet < Pet
30
+ validates_presence_of :name
31
+ end
32
+
16
33
  class Flea
17
34
  include MongoMapper::EmbeddedDocument
18
35
  key :name, String
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mm_partial_update
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 25
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 0
10
- version: 0.1.0
9
+ - 1
10
+ version: 0.1.1
11
11
  platform: ruby
12
12
  authors:
13
13
  - Nathan Stults
@@ -128,6 +128,7 @@ files:
128
128
  - lib/mm_partial_update/one_embedded_proxy.rb
129
129
  - lib/mm_partial_update/version.rb
130
130
  - lib/mm_partial_update/update_command.rb
131
+ - test/callbacks_support.rb
131
132
  - test/test_helper.rb
132
133
  - test/test_update_command.rb
133
134
  - test/functional/plugins/test_embedded_document.rb