switch_connection 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'switch_connection/error'
4
+
5
+ module SwitchConnection
6
+ class Proxy
7
+ attr_reader :initial_name
8
+
9
+ AVAILABLE_MODES = %i[master slave].freeze
10
+ DEFAULT_MODE = :master
11
+
12
+ def initialize(name)
13
+ @initial_name = name
14
+ @current_name = name
15
+ define_master_model(name)
16
+ define_slave_model(name)
17
+ @global_mode = DEFAULT_MODE
18
+ end
19
+
20
+ def define_master_model(name)
21
+ model_name = SwitchConnection.config.master_model_name(name)
22
+ if model_name
23
+ model = Class.new(ActiveRecord::Base)
24
+ Proxy.const_set(model_name, model)
25
+ master_database_name = SwitchConnection.config.master_database_name(name)
26
+ model.establish_connection(db_specific(master_database_name))
27
+ model
28
+ else
29
+ ActiveRecord::Base
30
+ end
31
+ end
32
+
33
+ def define_slave_model(name)
34
+ return unless SwitchConnection.config.slave_exist?(name)
35
+
36
+ slave_count = SwitchConnection.config.slave_count(name)
37
+ (0..(slave_count - 1)).map do |index|
38
+ model_name = SwitchConnection.config.slave_mode_name(name, index)
39
+ next ActiveRecord::Base unless model_name
40
+
41
+ model = Class.new(ActiveRecord::Base)
42
+ Proxy.const_set(model_name, model)
43
+ model.establish_connection(db_specific(SwitchConnection.config.slave_database_name(name, index)))
44
+ model
45
+ end
46
+ end
47
+
48
+ def db_specific(db_name)
49
+ base_config = ::ActiveRecord::Base.configurations.fetch(SwitchConnection.config.env)
50
+ return base_config if db_name == :default
51
+
52
+ db_name.to_s.split('.').inject(base_config) { |h, n| h[n] }
53
+ end
54
+
55
+ def thread_local_mode
56
+ Thread.current[:"switch_point_#{@current_name}_mode"]
57
+ end
58
+
59
+ def thread_local_mode=(mode)
60
+ Thread.current[:"switch_point_#{@current_name}_mode"] = mode
61
+ end
62
+ private :thread_local_mode=
63
+
64
+ def mode
65
+ thread_local_mode || @global_mode
66
+ end
67
+
68
+ def slave!
69
+ if thread_local_mode
70
+ self.thread_local_mode = :slave
71
+ else
72
+ @global_mode = :slave
73
+ end
74
+ end
75
+
76
+ def slave?
77
+ mode == :slave
78
+ end
79
+
80
+ def master!
81
+ if thread_local_mode
82
+ self.thread_local_mode = :master
83
+ else
84
+ @global_mode = :master
85
+ end
86
+ end
87
+
88
+ def master?
89
+ mode == :master
90
+ end
91
+
92
+ def with_slave(&block)
93
+ with_mode(:slave, &block)
94
+ end
95
+
96
+ def with_master(&block)
97
+ with_mode(:master, &block)
98
+ end
99
+
100
+ def with_mode(new_mode, &block)
101
+ unless AVAILABLE_MODES.include?(new_mode)
102
+ raise ArgumentError.new("Unknown mode: #{new_mode}")
103
+ end
104
+
105
+ saved_mode = thread_local_mode
106
+ self.thread_local_mode = new_mode
107
+ block.call
108
+ ensure
109
+ self.thread_local_mode = saved_mode
110
+ end
111
+
112
+ def switch_name(new_name, &block)
113
+ if block
114
+ begin
115
+ old_name = @current_name
116
+ @current_name = new_name
117
+ block.call
118
+ ensure
119
+ @current_name = old_name
120
+ end
121
+ else
122
+ @current_name = new_name
123
+ end
124
+ end
125
+
126
+ def reset_name!
127
+ @current_name = @initial_name
128
+ end
129
+
130
+ def model_for_connection
131
+ ProxyRepository.checkout(@current_name) # Ensure the target proxy is created
132
+ model_name = SwitchConnection.config.model_name(@current_name, mode)
133
+ if model_name
134
+ Proxy.const_get(model_name)
135
+ elsif mode == :slave
136
+ # When only master is specified, re-use master connection.
137
+ with_master do
138
+ model_for_connection
139
+ end
140
+ else
141
+ ActiveRecord::Base
142
+ end
143
+ end
144
+
145
+ def connection
146
+ model_for_connection.connection
147
+ end
148
+
149
+ def connected?
150
+ model_for_connection.connected?
151
+ end
152
+
153
+ def cache(&block)
154
+ r = with_slave { model_for_connection }
155
+ w = with_master { model_for_connection }
156
+ r.cache { w.cache(&block) }
157
+ end
158
+
159
+ def uncached(&block)
160
+ r = with_slave { model_for_connection }
161
+ w = with_master { model_for_connection }
162
+ r.uncached { w.uncached(&block) }
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'switch_connection/proxy'
5
+
6
+ module SwitchConnection
7
+ class ProxyRepository
8
+ include Singleton
9
+
10
+ def self.checkout(name)
11
+ instance.checkout(name)
12
+ end
13
+
14
+ def self.find(name)
15
+ instance.find(name)
16
+ end
17
+
18
+ def checkout(name)
19
+ proxies[name] ||= Proxy.new(name)
20
+ end
21
+
22
+ def find(name)
23
+ proxies.fetch(name)
24
+ end
25
+
26
+ def proxies
27
+ @proxies ||= {}
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwitchConnection
4
+ VERSION = '1.0.0'
5
+ end
data/spec/models.rb ADDED
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ SwitchConnection.configure do |config|
4
+ config.define_switch_point :main,
5
+ slaves: [:main_slave],
6
+ master: :main_master
7
+ config.define_switch_point :main2,
8
+ slaves: [:main2_slave],
9
+ master: :main2_master
10
+ config.define_switch_point :user,
11
+ slaves: [:user],
12
+ master: :user
13
+ config.define_switch_point :comment,
14
+ slaves: [:comment_slave],
15
+ master: :comment_master
16
+ config.define_switch_point :special,
17
+ slaves: [:main_slave_special],
18
+ master: :main_master
19
+ config.define_switch_point :nanika1,
20
+ slaves: [:main_slave],
21
+ master: :main_master
22
+ config.define_switch_point :nanika2,
23
+ slaves: [:main_slave],
24
+ master: :main_master
25
+ config.define_switch_point :nanika3,
26
+ master: :comment_master
27
+ end
28
+
29
+ require 'active_record'
30
+
31
+ class Book < ActiveRecord::Base
32
+ use_switch_point :main
33
+ after_save :do_after_save
34
+
35
+ private
36
+
37
+ def do_after_save; end
38
+ end
39
+
40
+ class Book2 < ActiveRecord::Base
41
+ use_switch_point :main
42
+ end
43
+
44
+ class Book3 < ActiveRecord::Base
45
+ use_switch_point :main2
46
+ end
47
+
48
+ class Publisher < ActiveRecord::Base
49
+ use_switch_point :main
50
+ end
51
+
52
+ class Comment < ActiveRecord::Base
53
+ use_switch_point :comment
54
+ end
55
+
56
+ class User < ActiveRecord::Base
57
+ use_switch_point :user
58
+ end
59
+
60
+ class BigData < ActiveRecord::Base
61
+ use_switch_point :special
62
+ end
63
+
64
+ class Note < ActiveRecord::Base
65
+ end
66
+
67
+ class Nanika1 < ActiveRecord::Base
68
+ use_switch_point :nanika1
69
+ end
70
+
71
+ class Nanika2 < ActiveRecord::Base
72
+ use_switch_point :nanika2
73
+ end
74
+
75
+ class Nanika3 < ActiveRecord::Base
76
+ use_switch_point :nanika3
77
+ end
78
+
79
+ class AbstractNanika < ActiveRecord::Base
80
+ use_switch_point :main
81
+ self.abstract_class = true
82
+ end
83
+
84
+ class DerivedNanika1 < AbstractNanika
85
+ end
86
+
87
+ class DerivedNanika2 < AbstractNanika
88
+ use_switch_point :main2
89
+ end
90
+
91
+ base =
92
+ if RUBY_PLATFORM == 'java'
93
+ { adapter: 'jdbcsqlite3' }
94
+ else
95
+ { adapter: 'sqlite3', pool: 10 }
96
+ end
97
+
98
+ databases = {
99
+ test: {
100
+ 'main_slave' => base.merge(database: 'main_slave.sqlite3'),
101
+ 'main_master' => base.merge(database: 'main_master.sqlite3'),
102
+ 'main2_slave' => base.merge(database: 'main2_slave.sqlite3'),
103
+ 'main2_master' => base.merge(database: 'main2_master.sqlite3'),
104
+ 'main_slave_special' => base.merge(database: 'main_slave_special.sqlite3'),
105
+ 'user' => base.merge(database: 'user.sqlite3'),
106
+ 'comment_slave' => base.merge(database: 'comment_slave.sqlite3'),
107
+ 'comment_master' => base.merge(database: 'comment_master.sqlite3'),
108
+ 'default' => base.merge(database: 'default.sqlite3')
109
+ }
110
+ }
111
+ ActiveRecord::Base.configurations =
112
+ # ActiveRecord.gem_version was introduced in ActiveRecord 4.0
113
+ if ActiveRecord.respond_to?(:gem_version) && ActiveRecord.gem_version >= Gem::Version.new('5.1.0')
114
+ { 'test' => databases }
115
+ else
116
+ databases
117
+ end
118
+
119
+ default_database_config = ActiveRecord::Base.configurations[SwitchConnection.config.env]['default']
120
+ ActiveRecord::Base.establish_connection(default_database_config)
121
+
122
+ # XXX: Check connection laziness
123
+ [Book, User, Note, Nanika1, ActiveRecord::Base].each do |model|
124
+ if model.connected?
125
+ raise "ActiveRecord::Base didn't establish connection lazily!"
126
+ end
127
+ end
128
+ ActiveRecord::Base.connection # Create connection
129
+
130
+ [Book, User, Nanika3].each do |model|
131
+ model.with_master do
132
+ if model.switch_point_proxy.connected?
133
+ raise "#{model.name} didn't establish connection lazily!"
134
+ end
135
+ end
136
+ model.with_slave do
137
+ if model.switch_point_proxy.connected?
138
+ raise "#{model.name} didn't establish connection lazily!"
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ ENV['RAILS_ENV'] ||= 'test'
4
+
5
+ require 'coveralls'
6
+ require 'simplecov'
7
+
8
+ SimpleCov.formatters = [
9
+ SimpleCov::Formatter::HTMLFormatter,
10
+ Coveralls::SimpleCov::Formatter,
11
+ ]
12
+ SimpleCov.start do
13
+ add_filter Bundler.bundle_path.to_s
14
+ add_filter File.dirname(__FILE__)
15
+ end
16
+
17
+ require 'switch_connection'
18
+ require 'models'
19
+
20
+ RSpec.configure do |config|
21
+ config.filter_run :focus
22
+ config.run_all_when_everything_filtered = true
23
+
24
+ if config.files_to_run.one?
25
+ config.full_backtrace = true
26
+ config.default_formatter = 'doc'
27
+ end
28
+
29
+ config.order = :random
30
+ Kernel.srand config.seed
31
+
32
+ config.expect_with :rspec do |expectations|
33
+ expectations.syntax = :expect
34
+ end
35
+
36
+ config.mock_with :rspec do |mocks|
37
+ mocks.syntax = :expect
38
+ mocks.verify_partial_doubles = true
39
+ end
40
+
41
+ config.before(:suite) do
42
+ Book.with_master do
43
+ Book.connection.execute('CREATE TABLE books (id integer primary key autoincrement)')
44
+ end
45
+
46
+ Book2.with_master do
47
+ Book2.connection.execute('CREATE TABLE book2s (id integer primary key autoincrement)')
48
+ end
49
+
50
+ FileUtils.cp('main_master.sqlite3', 'main_slave.sqlite3')
51
+
52
+ Book3.with_master do
53
+ Book3.connection.execute('CREATE TABLE book3s (id integer primary key autoincrement)')
54
+ end
55
+
56
+ FileUtils.cp('main2_master.sqlite3', 'main2_slave.sqlite3')
57
+
58
+ Note.connection.execute('CREATE TABLE notes (id integer primary key autoincrement)')
59
+
60
+ Nanika3.connection.execute('CREATE TABLE nanika3s (id integer primary key)')
61
+ end
62
+
63
+ config.after(:suite) do
64
+ ActiveRecord::Base.configurations[SwitchConnection.config.env].each_value do |c|
65
+ FileUtils.rm_f(c[:database])
66
+ end
67
+ end
68
+
69
+ config.after(:each) do
70
+ Book.with_master do
71
+ Book.delete_all
72
+ end
73
+ FileUtils.cp('main_master.sqlite3', 'main_slave.sqlite3')
74
+
75
+ Nanika3.delete_all
76
+ end
77
+ end
78
+
79
+ RSpec::Matchers.define :connect_to do |expected|
80
+ database_name = lambda do |model|
81
+ model.connection.pool.spec.config[:database]
82
+ end
83
+
84
+ match do |actual|
85
+ database_name.call(actual) == expected
86
+ end
87
+
88
+ failure_message do |actual|
89
+ "expected #{actual.name} to connect to #{expected} but connected to #{database_name.call(actual)}"
90
+ end
91
+ end
@@ -0,0 +1,402 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe SwitchConnection::Model do
4
+ describe '.use_switch_point' do
5
+ after do
6
+ Book.use_switch_point :main
7
+ end
8
+
9
+ it 'changes connection' do
10
+ expect(Book).to connect_to('main_master.sqlite3')
11
+ Book.use_switch_point :comment
12
+ expect(Book).to connect_to('comment_master.sqlite3')
13
+
14
+ Thread.start do
15
+ Book.with_switch_point(:main) do
16
+ expect(Book).to connect_to('main_master.sqlite3')
17
+ end
18
+ Book.with_switch_point(:comment) do
19
+ expect(Book).to connect_to('comment_master.sqlite3')
20
+ end
21
+ end.join
22
+ end
23
+
24
+ context 'with non-existing switch point name' do
25
+ it 'raises error' do
26
+ expect {
27
+ Class.new(ActiveRecord::Base) do
28
+ use_switch_point :not_found
29
+ end
30
+ }.to raise_error(KeyError)
31
+ end
32
+ end
33
+ end
34
+
35
+ describe '.connection' do
36
+ it 'returns master connection by default' do
37
+ expect(Book).to connect_to('main_master.sqlite3')
38
+ expect(Publisher).to connect_to('main_master.sqlite3')
39
+ expect(User).to connect_to('user.sqlite3')
40
+ expect(Comment).to connect_to('comment_master.sqlite3')
41
+ expect(Note).to connect_to('default.sqlite3')
42
+ expect(Book.switch_point_proxy).to be_master
43
+ end
44
+
45
+ context 'when auto_master is disabled' do
46
+ it 'raises error when destructive query is requested in slave mode' do
47
+ expect { Book.create }.to_not raise_error
48
+ expect { Book.with_master { Book.create } }.to_not raise_error
49
+ end
50
+ end
51
+
52
+ context 'when auto_master is enabled' do
53
+ around do |example|
54
+ SwitchConnection.configure do |config|
55
+ config.auto_master = true
56
+ end
57
+ example.run
58
+ SwitchConnection.configure do |config|
59
+ config.auto_master = false
60
+ end
61
+ end
62
+
63
+ it 'executes after_save callback in slave mode!' do
64
+ book = Book.new
65
+ expect(book).to receive(:do_after_save) {
66
+ expect(Book.switch_point_proxy).to be_master
67
+ expect(Book.connection.open_transactions).to eq(1)
68
+ }
69
+ book.save!
70
+ end
71
+ end
72
+
73
+ it 'works with newly checked-out connection' do
74
+ Thread.start do
75
+ Book.with_master do
76
+ Book.create
77
+ end
78
+ Book.with_slave { expect(Book.count).to eq(0) }
79
+ Book.with_master { expect(Book.count).to eq(1) }
80
+ end.join
81
+ end
82
+
83
+ context 'without switch_point configuration' do
84
+ it 'returns default connection' do
85
+ expect(Note.connection).to equal(ActiveRecord::Base.connection)
86
+ end
87
+ end
88
+
89
+ context 'with the same switch point name' do
90
+ it 'shares connection' do
91
+ expect(Book.connection).to equal(Publisher.connection)
92
+ end
93
+ end
94
+
95
+ context 'with the same database name' do
96
+ it 'does NOT shares a connection' do
97
+ expect(Book.connection).to_not equal(BigData.connection)
98
+ Book.with_master do
99
+ BigData.with_master do
100
+ expect(Book.connection).to_not equal(BigData.connection)
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ context 'when superclass uses use_switch_point' do
107
+ context 'without use_switch_point in derived class' do
108
+ it 'inherits switch_point configuration' do
109
+ expect(DerivedNanika1).to connect_to('main_master.sqlite3')
110
+ end
111
+
112
+ it 'shares connection with superclass' do
113
+ expect(DerivedNanika1.connection).to equal(AbstractNanika.connection)
114
+ end
115
+ end
116
+
117
+ context 'with use_switch_point in derived class' do
118
+ it 'overrides superclass' do
119
+ expect(DerivedNanika2).to connect_to('main2_master.sqlite3')
120
+ end
121
+ end
122
+
123
+ context 'when superclass changes switch_point' do
124
+ after do
125
+ AbstractNanika.use_switch_point :main
126
+ end
127
+
128
+ it 'follows' do
129
+ AbstractNanika.use_switch_point :main2
130
+ expect(DerivedNanika1).to connect_to('main2_master.sqlite3')
131
+ end
132
+ end
133
+ end
134
+
135
+ context 'without :master' do
136
+ it 'sends destructive queries to ActiveRecord::Base' do
137
+ expect(Nanika1).to connect_to('main_master.sqlite3')
138
+ Nanika1.with_master do
139
+ expect(Nanika1).to connect_to('main_master.sqlite3')
140
+ end
141
+ end
142
+ end
143
+
144
+ context 'without :slave' do
145
+ it 'sends all queries to :master' do
146
+ expect(Nanika3).to connect_to('comment_master.sqlite3')
147
+ Nanika3.with_master do
148
+ expect(Nanika3).to connect_to('comment_master.sqlite3')
149
+ Nanika3.create
150
+ end
151
+ expect(Nanika3.count).to eq(1)
152
+ expect(Nanika3.with_slave { Nanika3.connection }).to equal(Nanika3.with_master { Nanika3.connection })
153
+ end
154
+ end
155
+ end
156
+
157
+ describe '.with_master' do
158
+ it 'changes connection locally' do
159
+ Book.with_master do
160
+ expect(Book).to connect_to('main_master.sqlite3')
161
+ expect(Book.switch_point_proxy).to be_master
162
+ end
163
+ expect(Book).to connect_to('main_master.sqlite3')
164
+ expect(Book.switch_point_proxy).to be_master
165
+ end
166
+
167
+ it 'changes connection locally' do
168
+ Book.with_switch_point(:comment) do
169
+ expect(Book.switch_point_proxy).to be_master
170
+ expect(Book).to connect_to('comment_master.sqlite3')
171
+ Thread.start do
172
+ expect(Book).to connect_to('main_master.sqlite3')
173
+ end.join
174
+ end
175
+
176
+ expect(Book.switch_point_proxy).to be_master
177
+ expect(Book).to connect_to('main_master.sqlite3')
178
+ end
179
+
180
+ it 'affects to other models with the same switch point' do
181
+ Book.with_master do
182
+ expect(Publisher).to connect_to('main_master.sqlite3')
183
+ end
184
+ expect(Publisher).to connect_to('main_master.sqlite3')
185
+ end
186
+
187
+ it 'does not affect to other models with different switch point' do
188
+ Book.with_master do
189
+ expect(Comment).to connect_to('comment_master.sqlite3')
190
+ end
191
+ end
192
+
193
+ context 'with the same switch point' do
194
+ it 'shares connection' do
195
+ Book.with_master do
196
+ expect(Book.connection).to equal(Publisher.connection)
197
+ end
198
+ end
199
+ end
200
+
201
+ context 'without use_switch_point' do
202
+ it 'raises error' do
203
+ expect { Note.with_master { :bypass } }.to raise_error(SwitchConnection::UnconfiguredError)
204
+ end
205
+ end
206
+
207
+ it 'affects thread-locally' do
208
+ Book.with_slave do
209
+ expect(Book).to connect_to('main_slave.sqlite3')
210
+ Thread.start do
211
+ expect(Book).to connect_to('main_master.sqlite3')
212
+ end.join
213
+ end
214
+ end
215
+ end
216
+
217
+ describe '#with_master' do
218
+ it 'behaves like .with_master' do
219
+ book = Book.with_master { Book.create! }
220
+ book.with_master do
221
+ expect(Book).to connect_to('main_master.sqlite3')
222
+ end
223
+ expect(Book).to connect_to('main_master.sqlite3')
224
+ end
225
+ end
226
+
227
+ describe '.with_slave' do
228
+ context 'when master! is called globally' do
229
+ before do
230
+ SwitchConnection.master!(:main)
231
+ end
232
+
233
+ it 'locally overwrites global mode' do
234
+ Book.with_slave do
235
+ expect(Book).to connect_to('main_slave.sqlite3')
236
+ end
237
+ expect(Book).to connect_to('main_master.sqlite3')
238
+ end
239
+ end
240
+ end
241
+
242
+ describe '#with_slave' do
243
+ it 'behaves like .with_slave' do
244
+ book = Book.create!
245
+ book.with_slave do
246
+ expect(Book).to connect_to('main_slave.sqlite3')
247
+ end
248
+ expect(Book).to connect_to('main_master.sqlite3')
249
+ end
250
+ end
251
+
252
+ describe '#with_mode' do
253
+ it 'raises error if unknown mode is given' do
254
+ expect { SwitchConnection::ProxyRepository.checkout(:main).with_mode(:typo) }.to raise_error(ArgumentError)
255
+ end
256
+ end
257
+
258
+ describe '.switch_name' do
259
+ after do
260
+ Book.switch_point_proxy.reset_name!
261
+ end
262
+
263
+ it 'switches proxy configuration' do
264
+ Book.switch_point_proxy.switch_name(:comment)
265
+ expect(Book).to connect_to('comment_master.sqlite3')
266
+ expect(Publisher).to connect_to('comment_master.sqlite3')
267
+ end
268
+
269
+ context 'with block' do
270
+ it 'switches proxy configuration locally' do
271
+ Book.switch_point_proxy.switch_name(:comment) do
272
+ expect(Book).to connect_to('comment_master.sqlite3')
273
+ expect(Publisher).to connect_to('comment_master.sqlite3')
274
+ end
275
+ expect(Book).to connect_to('main_master.sqlite3')
276
+ expect(Publisher).to connect_to('main_master.sqlite3')
277
+ end
278
+ end
279
+ end
280
+
281
+ describe '.transaction_with' do
282
+ context 'when each model has a same master' do
283
+ before do
284
+ @before_book_count = Book.count
285
+ @before_book2_count = Book2.count
286
+
287
+ Book.transaction_with(Book2) do
288
+ Book.create
289
+ Book2.create
290
+ end
291
+
292
+ @after_book_count = Book.with_master do
293
+ Book.count
294
+ end
295
+ @after_book2_count = Book2.with_master do
296
+ Book2.count
297
+ end
298
+ end
299
+
300
+ it 'should create a new record' do
301
+ expect(
302
+ Book.with_master do
303
+ Book.count
304
+ end
305
+ ).to be > @before_book_count
306
+
307
+ expect(
308
+ Book2.with_master do
309
+ Book2.count
310
+ end
311
+ ).to be > @before_book2_count
312
+ end
313
+ end
314
+
315
+ context 'when each model has a other master' do
316
+ it {
317
+ expect {
318
+ Book.transaction_with(Book3) do
319
+ Book.create
320
+ Book3.create
321
+ end
322
+ }.to raise_error(SwitchConnection::Error)
323
+ }
324
+ end
325
+
326
+ context 'when raise exception in transaction that include some model, and models each have other master' do
327
+ before do
328
+ @before_book_count = Book.count
329
+ @before_book3_count = Book3.count
330
+
331
+ Book.transaction_with(Book2) do
332
+ Book.create
333
+ Book3.with_master do
334
+ Book3.create
335
+ end
336
+ raise ActiveRecord::Rollback
337
+ end
338
+ end
339
+
340
+ it 'Book should not create a new record (rollbacked)' do
341
+ expect(
342
+ Book.with_master do
343
+ Book.count
344
+ end
345
+ ).to eq @before_book_count
346
+ end
347
+
348
+ it 'Book3 should create a new record (not rollbacked)' do
349
+ expect(
350
+ Book3.with_master do
351
+ Book3.count
352
+ end
353
+ ).to be > @before_book3_count
354
+ end
355
+ end
356
+
357
+ context 'when nested transaction_with then parent transaction rollbacked' do
358
+ before do
359
+ @before_book_count = Book.count
360
+ @before_book3_count = Book3.count
361
+
362
+ Book.transaction_with do
363
+ Book.create
364
+
365
+ Book3.transaction_with do
366
+ Book3.create
367
+ end
368
+
369
+ raise ActiveRecord::Rollback
370
+ end
371
+
372
+ it {
373
+ expect(
374
+ Book.with_master do
375
+ Book.count
376
+ end
377
+ ).to be = @before_book_count
378
+
379
+ expect(
380
+ Book3.with_master do
381
+ Book3.count
382
+ end
383
+ ).to be > @before_book3_count
384
+ }
385
+ end
386
+ end
387
+ end
388
+
389
+ describe '#transaction_with' do
390
+ it 'behaves like .transaction_with' do
391
+ book = Book.with_master { Book.create! }
392
+ expect(Book.with_master { Book.count }).to eq(1)
393
+ book.transaction_with(Book2) do
394
+ Book.create!
395
+ raise ActiveRecord::Rollback
396
+ end
397
+ expect(Book.with_master { Book.count }).to eq(1)
398
+
399
+ expect { book.transaction_with(Book3) {} }.to raise_error(SwitchConnection::Error)
400
+ end
401
+ end
402
+ end