mongoid_optimistic_locking 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.travis.yml CHANGED
@@ -1,5 +1,4 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.8.7
4
3
  - 1.9.2
5
4
  - 1.9.3
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- # Specify your gem's dependencies in mongoid_optimistic_locking.gemspec
4
3
  gemspec
data/README.md CHANGED
@@ -6,9 +6,9 @@ The gem is an addon over [Mongoid ODM](http://mongoid.org/) and is based on [Act
6
6
 
7
7
  ## Compatibility
8
8
 
9
- So far it works with the Rails 3 and Mongoid 2.4.
9
+ Works with *Mongoid 3*.
10
10
 
11
- Created branch for edge *Mongoid 3*.
11
+ For Mongoid 2 use version 0.0.2
12
12
 
13
13
  ## Rails 3 Installation
14
14
 
@@ -23,7 +23,6 @@ To use it, all you have to do is add include `Mongoid::OptimisticLocking`:
23
23
  class Post
24
24
  include Mongoid::Document
25
25
  include Mongoid::OptimisticLocking
26
-
27
26
  field :text
28
27
  end
29
28
 
@@ -47,6 +46,28 @@ For example:
47
46
 
48
47
  That's it!
49
48
 
49
+ ## Embedded Document Caveats
50
+
51
+ While `Mongoid::OptimisticLocking` can be used to some degree within embedded documents, there are certain limitations due to Mongoid's document embedding callback structure. Consider the following example:
52
+
53
+ class Post
54
+ include Mongoid::Document
55
+ field :text
56
+ embeds_many :comments
57
+ end
58
+
59
+ class Comment
60
+ include Mongoid::Document
61
+ include Mongoid::OptimisticLocking
62
+ embedded_in :post
63
+ field :text
64
+ end
65
+
66
+ post = Post.new
67
+ comment = post.comments.build(:text => 'hello')
68
+ comment.save # will use optimistic locking checks
69
+ post.save # will not use optimistic locking checks
70
+
50
71
  ## Open sourced by
51
72
 
52
73
  [Boxee](http://www.boxee.tv)
@@ -55,3 +76,5 @@ That's it!
55
76
  [Mongo Developer FAQ - How do I do transactions/locking?](http://docs.mongodb.org/manual/faq/developers/#how-do-i-do-transactions-and-locking-in-mongodb)
56
77
 
57
78
  [Mongo Atomic Operations - "Update if Current"](http://www.mongodb.org/display/DOCS/Atomic+Operations#AtomicOperations-%22UpdateifCurrent%22)
79
+
80
+ [Presentation from "Startup Day" on Mongoid Optimistic Locking](https://speakerdeck.com/u/burgalon/p/mongoid-optimistic-locking)
@@ -2,19 +2,20 @@ module Mongoid
2
2
  module OptimisticLocking
3
3
  module Operations
4
4
 
5
- def insert(*args)
5
+ def insert(options = {})
6
6
  return super unless optimistic_locking?
7
7
  increment_lock_version do
8
8
  super
9
9
  end
10
10
  end
11
11
 
12
- def update(*args)
12
+ def update(options = {})
13
13
  return super unless optimistic_locking?
14
14
  set_lock_version_for_selector do
15
15
  increment_lock_version do
16
16
  result = super
17
- unless Mongoid.database.command({:getlasterror => 1})['updatedExisting']
17
+ getlasterror = mongo_session.command({:getlasterror => 1})
18
+ if result && !getlasterror['updatedExisting']
18
19
  raise Mongoid::Errors::StaleDocument.new('update', self)
19
20
  end
20
21
  result
@@ -22,15 +23,16 @@ module Mongoid
22
23
  end
23
24
  end
24
25
 
25
- def remove(*args)
26
- return super unless optimistic_locking?
27
- set_lock_version_for_selector do
28
- result = super
29
- unless Mongoid.database.command({:getlasterror => 1})['updatedExisting']
30
- raise Mongoid::Errors::StaleDocument.new('destroy', self)
31
- end
32
- result
26
+ def remove(options = {})
27
+ return super unless optimistic_locking? && persisted?
28
+ # we need to just see if the document exists and got updated with
29
+ # a higher lock version
30
+ existing = _reload # get the current root or embedded document
31
+ if existing.present? && existing['_lock_version'] &&
32
+ existing['_lock_version'].to_i > _lock_version.to_i
33
+ raise Mongoid::Errors::StaleDocument.new('destroy', self)
33
34
  end
35
+ super
34
36
  end
35
37
 
36
38
  def atomic_selector
@@ -1,5 +1,5 @@
1
1
  module Mongoid
2
2
  module OptimisticLocking
3
- VERSION = '0.0.2'
3
+ VERSION = '0.0.3'
4
4
  end
5
5
  end
@@ -11,10 +11,9 @@ Gem::Specification.new do |s|
11
11
  s.summary = %q{Allows optimisitic locking for Mongoid models}
12
12
  s.description = %q{Allows optimisitic locking for Mongoid models. See https://github.com/burgalon/mongoid_optimistic_locking}
13
13
 
14
- s.add_dependency 'mongoid', '~> 2.4'
14
+ s.add_dependency 'mongoid', '>= 3.0.0rc'
15
15
  s.add_development_dependency 'rake', '~> 0.9.0'
16
16
  s.add_development_dependency 'rspec', '~> 2.6'
17
- s.add_development_dependency 'bson_ext', '~> 1.5'
18
17
 
19
18
  s.rubyforge_project = "mongoid_optimistic_locking"
20
19
 
@@ -2,106 +2,296 @@ require 'spec_helper'
2
2
 
3
3
  describe Mongoid::OptimisticLocking do
4
4
 
5
- context 'without optimistic locking' do
5
+ COLLECTIONS = %w(Company Person Car State Flag City)
6
6
 
7
- let(:company_class) do
8
- Class.new do
9
- include Mongoid::Document
10
- self.collection_name = 'companies'
11
- field :name
12
- end
7
+ before(:all) do
8
+
9
+ class Company
10
+ include Mongoid::Document
11
+ field :name
12
+ has_many :people, :class_name => 'Person', :dependent => :destroy
13
+ end
14
+
15
+ class Person
16
+ include Mongoid::Document
17
+ include Mongoid::OptimisticLocking
18
+ field :name
19
+ has_many :cars, :dependent => :destroy
13
20
  end
14
21
 
15
- it 'should allow collisions' do
16
- c1 = company_class.create!(:name => 'Acme')
17
- c2 = company_class.find(c1.id)
22
+ class Car
23
+ include Mongoid::Document
24
+ include Mongoid::OptimisticLocking
25
+ field :color
26
+ belongs_to :person
27
+ end
28
+
29
+ class State
30
+ include Mongoid::Document
31
+ field :name
32
+ embeds_one :flag, :inverse_of => :state
33
+ embeds_many :cities, :class_name => 'City', :inverse_of => :state
34
+ end
35
+
36
+ class Flag
37
+ include Mongoid::Document
38
+ include Mongoid::OptimisticLocking
39
+ embedded_in :state, :inverse_of => :flag
40
+ field :color
41
+ end
42
+
43
+ class City
44
+ include Mongoid::Document
45
+ include Mongoid::OptimisticLocking
46
+ embedded_in :state, :inverse_of => :cities
47
+ field :name
48
+ end
49
+
50
+ end
51
+
52
+ after(:all) { COLLECTIONS.each { |n| Object.send(:remove_const, n) } }
53
+ before { (COLLECTIONS - %w(Flag City)).each { |n| n.constantize.delete_all } }
54
+
55
+ context 'without optimistic locking' do
56
+
57
+ let!(:c1) { Company.create!(:name => 'Acme') }
58
+ let!(:c2) { Company.find(c1.id) }
59
+
60
+ it 'should allow collisions when saving' do
18
61
  c1.name = 'Biz'
19
- c1.save.should == true
62
+ c1.save.should be_true
20
63
  c2.name = 'Baz'
21
- c2.save.should == true
64
+ c2.save.should be_true
65
+ end
66
+
67
+ it 'should allow collisions when destroying' do
68
+ c1.destroy.should be_true
69
+ c2.destroy.should be_true
22
70
  end
23
71
 
24
72
  end
25
73
 
26
- context 'three instances of the same document' do
74
+ context 'optimistic locking on a document' do
27
75
 
28
- let(:person_class) do
29
- Class.new do
30
- include Mongoid::Document
31
- include Mongoid::OptimisticLocking
32
- self.collection_name = 'people'
33
- field :name
34
- end
76
+ let(:person) { Person.create!(:name => 'Bob') }
77
+
78
+ it 'should allow creating' do
79
+ person._lock_version.should == 1
80
+ person.save.should be_true
81
+ person._lock_version.should == 2
82
+ Person.count.should == 1
83
+ end
84
+
85
+ it 'should allow updating' do
86
+ person.name = 'Chris'
87
+ person.save.should be_true
88
+ person._lock_version.should == 2
89
+ end
90
+
91
+ it 'should allow destroying after create' do
92
+ person.destroy.should be_true
93
+ person.should be_frozen
94
+ person._lock_version.should == 1
95
+ Person.count.should == 0
35
96
  end
36
97
 
37
- before do
38
- @p1 = person_class.create!(:name => 'Bob')
39
- @p2 = person_class.find(@p1.id)
40
- @p3 = person_class.find(@p1.id)
98
+ it 'should allow destroying from find' do
99
+ another = Person.find(person.id)
100
+ another.destroy.should be_true
101
+ another.should be_frozen
102
+ another._lock_version.should == 1
103
+ Person.count.should == 0
41
104
  end
42
105
 
43
- context 'after updating the first' do
106
+ context 'after updating the first of 3 instances of the same document' do
44
107
 
45
108
  before do
109
+ @p1 = person
110
+ @p2 = Person.find(@p1.id)
111
+ @p3 = Person.find(@p1.id)
46
112
  @p1.name = 'Michael'
47
- @p1.save.should == true
113
+ @p1.save.should be_true
48
114
  end
49
115
 
50
- it 'should fail when updating the second' do
116
+ it 'should raise an exception when updating the second' do
51
117
  expect {
52
118
  @p2.name = 'George'
53
119
  @p2.save
54
120
  }.to raise_error(Mongoid::Errors::StaleDocument)
55
121
  end
56
-
122
+
57
123
  it 'should succeed when updating the second without locking' do
58
124
  @p2.name = 'George'
59
- @p2.unlocked.save.should == true
125
+ @p2.unlocked.save.should be_true
60
126
  end
61
-
127
+
62
128
  it 'should succeed when updating the second without locking, ' +
63
- 'then fail when updating the third' do
129
+ 'then raise an exception when updating the third' do
64
130
  @p2.name = 'George'
65
- @p2.unlocked.save.should == true
131
+ @p2.unlocked.save.should be_true
66
132
  expect {
67
133
  @p3.name = 'Sally'
68
134
  @p3.save
69
135
  }.to raise_error(Mongoid::Errors::StaleDocument)
70
136
  end
71
137
 
72
- it 'should fail when destroying the second' do
138
+ it 'should raise an exception when destroying the second' do
73
139
  expect {
74
140
  @p2.destroy
75
141
  }.to raise_error(Mongoid::Errors::StaleDocument)
142
+ Person.count.should == 1
76
143
  end
77
144
 
78
145
  it 'should succeed when destroying the second without locking' do
79
146
  @p2.unlocked.destroy
147
+ Person.count.should == 0
80
148
  end
81
149
 
82
150
  it 'should succeed when destroying the second without locking, ' +
83
- 'then fail when destroying the third' do
151
+ 'and succeed when destroying the third with locking' do
84
152
  @p2.unlocked.destroy
85
- expect {
86
- @p3.destroy
87
- }.to raise_error(Mongoid::Errors::StaleDocument)
153
+ Person.count.should == 0
154
+ @p3.destroy.should
88
155
  end
89
156
 
90
157
  end
91
158
 
159
+ it 'should destroy dependents with dependent destroy option' do
160
+ Car.count.should == 0
161
+ person.cars.create(:color => 'Red')
162
+ person.destroy
163
+ Car.count.should == 0
164
+ end
165
+
166
+ it 'should destroy dependents with dependent destroy option even when they are concurrently edited' do
167
+ car1 = person.cars.create(:color => 'Red')
168
+ car2 = Car.find(car1.id)
169
+ car2.update_attribute :color, 'Green'
170
+ person.reload # important
171
+ person.destroy
172
+ Person.count.should == 0
173
+ Car.count.should == 0
174
+ end
175
+
92
176
  it 'should give a deprecation warning for #save_optimistic!' do
93
177
  ::ActiveSupport::Deprecation.should_receive(:warn).once
94
- @p1.save_optimistic!
178
+ person.save_optimistic!
95
179
  end
96
180
 
97
- it 'should give a deprecation warning for including Mongoid::Lockable' do
98
- ::ActiveSupport::Deprecation.should_receive(:warn).once
99
- Class.new do
100
- include Mongoid::Document
101
- include Mongoid::Lockable
102
- end
181
+ end
182
+
183
+ context 'optimistic locking on an embeds_one document' do
184
+
185
+ let!(:state) { State.new(:name => 'California') }
186
+
187
+ it 'should be savable with no embeds' do
188
+ state.name = 'Nevada'
189
+ state.save.should be_true
190
+ end
191
+
192
+ it 'should be savable with a built embed' do
193
+ state.build_flag(:color => 'Red')
194
+ state.save.should be_true
195
+ end
196
+
197
+ it 'should allow creating the embed' do
198
+ state.create_flag(:color => 'Red')
199
+ state.flag.should be_persisted
200
+ end
201
+
202
+ it 'should not increment the lock version when saving the base' do
203
+ state.build_flag(:color => 'Red')
204
+ state.flag._lock_version.should == 0
205
+ state.save
206
+ state.flag._lock_version.should == 0
207
+ end
208
+
209
+ it 'should increment lock version when saving the embed' do
210
+ state.build_flag(:color => 'Red')
211
+ state.flag._lock_version.should == 0
212
+ state.flag.save
213
+ state.flag._lock_version.should == 1
214
+ end
215
+
216
+ it 'should not raise an exception on saving the base when another process updated it' do
217
+ flag1 = state.create_flag(:color => 'Red')
218
+ flag2 = State.find(state.id).flag
219
+ flag1.color = 'Green'
220
+ state.save # doesn't increment lock version
221
+ flag2.update_attribute(:color, 'Purple').should be_true
222
+ end
223
+
224
+ it 'should raise an exception on update when another process updated it' do
225
+ flag1 = state.create_flag(:color => 'Red')
226
+ flag2 = State.find(state.id).flag
227
+ flag1.update_attribute :color, 'Green'
228
+ expect {
229
+ flag2.update_attribute :color, 'Purple'
230
+ }.to raise_error(Mongoid::Errors::StaleDocument)
231
+ end
232
+
233
+ it 'should raise an exception on destroy when another process updated it' do
234
+ flag1 = state.create_flag(:color => 'Red')
235
+ flag2 = State.find(state.id).flag
236
+ flag1.update_attribute :color, 'Green'
237
+ expect {
238
+ flag2.destroy
239
+ }.to raise_error(Mongoid::Errors::StaleDocument)
240
+ end
241
+
242
+ end
243
+
244
+ context 'optimistic locking on an embeds_many document' do
245
+
246
+ let!(:state) { State.create!(:name => 'California') }
247
+
248
+ it 'should be savable with no embeds' do
249
+ state.name = 'Nevada'
250
+ state.save.should be_true
251
+ end
252
+
253
+ it 'should be savable with a built embed' do
254
+ state.cities.build(:name => 'Los Angeles')
255
+ state.save.should be_true
256
+ end
257
+
258
+ it 'should allow embedded build to save' do
259
+ city = state.cities.build(:name => 'Los Angeles')
260
+ city.save.should be_true
261
+ end
262
+
263
+ it 'should allow embedded create' do
264
+ city = state.cities.create(:name => 'Los Angeles')
265
+ city.should be_persisted
103
266
  end
104
267
 
268
+ it 'should not increment the lock version when saving the base' do
269
+ city = state.cities.build(:name => 'Los Angeles')
270
+ city._lock_version.should == 0
271
+ state.save
272
+ city._lock_version.should == 0
273
+ end
274
+
275
+ it 'should increment the lock version when building and saving the embed' do
276
+ city = state.cities.build(:name => 'Los Angeles')
277
+ city._lock_version.should == 0
278
+ city.save
279
+ city._lock_version.should == 1
280
+ end
281
+
282
+ it 'should increment the lock version when creating the embed' do
283
+ city = state.cities.create(:name => 'Los Angeles')
284
+ city._lock_version.should == 1
285
+ end
286
+
287
+ end
288
+
289
+ it 'should give a deprecation warning for including Mongoid::Lockable' do
290
+ ::ActiveSupport::Deprecation.should_receive(:warn).once
291
+ Class.new do
292
+ include Mongoid::Document
293
+ include Mongoid::Lockable
294
+ end
105
295
  end
106
296
 
107
297
  end
data/spec/spec_helper.rb CHANGED
@@ -4,8 +4,9 @@ Bundler.setup
4
4
 
5
5
  require 'mongoid'
6
6
 
7
+ # Set the database that the spec suite connects to.
7
8
  Mongoid.configure do |config|
8
- config.master = Mongo::Connection.new.db("mongoid_optimistic_locking_test")
9
+ config.connect_to('mongoid_optimistic_locking_test')
9
10
  end
10
11
 
11
12
  $LOAD_PATH.unshift(File.dirname(__FILE__))
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid_optimistic_locking
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,22 +10,27 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-05-31 00:00:00.000000000 Z
13
+ date: 2013-03-05 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: mongoid
17
- requirement: &2161833600 !ruby/object:Gem::Requirement
17
+ requirement: !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
- - - ~>
20
+ - - ! '>='
21
21
  - !ruby/object:Gem::Version
22
- version: '2.4'
22
+ version: 3.0.0rc
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: *2161833600
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: 3.0.0rc
26
31
  - !ruby/object:Gem::Dependency
27
32
  name: rake
28
- requirement: &2161832880 !ruby/object:Gem::Requirement
33
+ requirement: !ruby/object:Gem::Requirement
29
34
  none: false
30
35
  requirements:
31
36
  - - ~>
@@ -33,10 +38,15 @@ dependencies:
33
38
  version: 0.9.0
34
39
  type: :development
35
40
  prerelease: false
36
- version_requirements: *2161832880
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: 0.9.0
37
47
  - !ruby/object:Gem::Dependency
38
48
  name: rspec
39
- requirement: &2161832180 !ruby/object:Gem::Requirement
49
+ requirement: !ruby/object:Gem::Requirement
40
50
  none: false
41
51
  requirements:
42
52
  - - ~>
@@ -44,18 +54,12 @@ dependencies:
44
54
  version: '2.6'
45
55
  type: :development
46
56
  prerelease: false
47
- version_requirements: *2161832180
48
- - !ruby/object:Gem::Dependency
49
- name: bson_ext
50
- requirement: &2161831560 !ruby/object:Gem::Requirement
57
+ version_requirements: !ruby/object:Gem::Requirement
51
58
  none: false
52
59
  requirements:
53
60
  - - ~>
54
61
  - !ruby/object:Gem::Version
55
- version: '1.5'
56
- type: :development
57
- prerelease: false
58
- version_requirements: *2161831560
62
+ version: '2.6'
59
63
  description: Allows optimisitic locking for Mongoid models. See https://github.com/burgalon/mongoid_optimistic_locking
60
64
  email:
61
65
  - burgalon@gmail.com
@@ -106,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
110
  version: '0'
107
111
  requirements: []
108
112
  rubyforge_project: mongoid_optimistic_locking
109
- rubygems_version: 1.8.10
113
+ rubygems_version: 1.8.24
110
114
  signing_key:
111
115
  specification_version: 3
112
116
  summary: Allows optimisitic locking for Mongoid models