n_plus_one_control 0.2.1 → 0.5.0
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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +1 -1
- data/README.md +127 -12
- data/lib/n_plus_one_control.rb +96 -5
- data/lib/n_plus_one_control/executor.rb +63 -23
- data/lib/n_plus_one_control/minitest.rb +24 -5
- data/lib/n_plus_one_control/railtie.rb +13 -0
- data/lib/n_plus_one_control/rspec.rb +4 -1
- data/lib/n_plus_one_control/rspec/context.rb +12 -11
- data/lib/n_plus_one_control/rspec/dsl.rb +24 -6
- data/lib/n_plus_one_control/rspec/matcher.rb +13 -5
- data/lib/n_plus_one_control/version.rb +1 -1
- metadata +19 -89
- data/.gitignore +0 -11
- data/.rspec +0 -2
- data/.rubocop.yml +0 -74
- data/.travis.yml +0 -5
- data/Gemfile +0 -4
- data/Rakefile +0 -13
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/n_plus_one_control.gemspec +0 -47
- data/spec/n_plus_one_control/executor_spec.rb +0 -65
- data/spec/n_plus_one_control/rspec_spec.rb +0 -84
- data/spec/n_plus_one_control_spec.rb +0 -9
- data/spec/spec_helper.rb +0 -30
- data/spec/support/post.rb +0 -19
- data/spec/support/user.rb +0 -17
- data/tests/minitest_test.rb +0 -63
- data/tests/test_helper.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 394ec848692af46dd43c3a03a3315349c2aea19d174ff05ededa1ef825839ee0
|
4
|
+
data.tar.gz: 7e2a271fe8ae173b117d1fc514e33b6f08aa7c528e4ea476f5360591a28da6f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f411b0c1539f517e0c4036570e274fe62327817722065551fce09307e15402c31185566343ddb9fd388324a71c5c8d7730fc938767397f5bbba60f103ffdccb
|
7
|
+
data.tar.gz: 21a4445b6dd7efa9dad9bbfe2ccf8708ae813e2e60ea424b60f7b00cf58bef1971d58b1392893f34e817a3834792b461536890ed38619a20e0f9bd770ab7ddfd
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
## master (unreleased)
|
2
|
+
|
3
|
+
## 0.5.0 (2020-09-07)
|
4
|
+
|
5
|
+
- **Ruby 2.5+ is required**. ([@palkan][])
|
6
|
+
|
7
|
+
- Add support for multiple backtrace lines in verbose output. ([@palkan][])
|
8
|
+
|
9
|
+
Could be specified via `NPLUSONE_BACKTRACE` env var.
|
10
|
+
|
11
|
+
- Add `NPLUSONE_TRUNCATE` env var to truncate queries in verbose mode. ([@palkan][])
|
12
|
+
|
13
|
+
- Support passing default filter via `NPLUSONE_FILTER` env var. ([@palkan][])
|
14
|
+
|
15
|
+
- Add location tracing to SQLs in verbose mode. ([@palkan][])
|
16
|
+
|
17
|
+
## 0.4.1 (2020-09-04)
|
18
|
+
|
19
|
+
- Enhance failure message by showing differences in table hits. ([@palkan][])
|
20
|
+
|
21
|
+
## 0.4.0 (2020-07-20)
|
22
|
+
|
23
|
+
- Make scale factor available in tests via `#current_scale` method. ([@Earendil95][])
|
24
|
+
|
25
|
+
- Start keeping a changelog. ([@palkan][])
|
26
|
+
|
27
|
+
[@Earendil95]: https://github.com/Earendil95
|
28
|
+
[@palkan]: https://github.com/palkan
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
[](https://rubygems.org/gems/n_plus_one_control)
|
1
|
+
[](https://rubygems.org/gems/n_plus_one_control)
|
2
|
+

|
2
3
|
|
3
4
|
# N + 1 Control
|
4
5
|
|
@@ -31,7 +32,7 @@ Add this line to your application's Gemfile:
|
|
31
32
|
|
32
33
|
```ruby
|
33
34
|
group :test do
|
34
|
-
gem
|
35
|
+
gem "n_plus_one_control"
|
35
36
|
end
|
36
37
|
```
|
37
38
|
|
@@ -47,8 +48,6 @@ First, add NPlusOneControl to your `spec_helper.rb`:
|
|
47
48
|
|
48
49
|
```ruby
|
49
50
|
# spec_helper.rb
|
50
|
-
...
|
51
|
-
|
52
51
|
require "n_plus_one_control/rspec"
|
53
52
|
```
|
54
53
|
|
@@ -86,10 +85,30 @@ Availables modifiers:
|
|
86
85
|
```ruby
|
87
86
|
# You can specify the RegExp to filter queries.
|
88
87
|
# By default, it only considers SELECT queries.
|
89
|
-
expect {
|
88
|
+
expect { subject }.to perform_constant_number_of_queries.matching(/INSERT/)
|
90
89
|
|
91
90
|
# You can also provide custom scale factors
|
92
|
-
expect {
|
91
|
+
expect { subject }.to perform_constant_number_of_queries.with_scale_factors(10, 100)
|
92
|
+
```
|
93
|
+
|
94
|
+
#### Using scale factor in spec
|
95
|
+
|
96
|
+
Let's suppose your action accepts parameter, which can make impact on the number of returned records:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
get :index, params: {per_page: 10}
|
100
|
+
```
|
101
|
+
|
102
|
+
Then it is enough to just change `per_page` parameter between executions and do not recreate records in DB. For this purpose, you can use `current_scale` method in your example:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
context "N+1", :n_plus_one do
|
106
|
+
before { create_list :post, 3 }
|
107
|
+
|
108
|
+
specify do
|
109
|
+
expect { get :index, params: {per_page: current_scale} }.to perform_constant_number_of_queries
|
110
|
+
end
|
111
|
+
end
|
93
112
|
```
|
94
113
|
|
95
114
|
### Minitest
|
@@ -98,8 +117,6 @@ First, add NPlusOneControl to your `test_helper.rb`:
|
|
98
117
|
|
99
118
|
```ruby
|
100
119
|
# test_helper.rb
|
101
|
-
...
|
102
|
-
|
103
120
|
require "n_plus_one_control/minitest"
|
104
121
|
```
|
105
122
|
|
@@ -133,6 +150,12 @@ assert_perform_constant_number_of_queries(
|
|
133
150
|
end
|
134
151
|
```
|
135
152
|
|
153
|
+
It's possible to specify a filter via `NPLUSONE_FILTER` env var, e.g.:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
NPLUSONE_FILTER = users bundle exec rake test
|
157
|
+
```
|
158
|
+
|
136
159
|
You can also specify `populate` as a test class instance method:
|
137
160
|
|
138
161
|
```ruby
|
@@ -145,6 +168,68 @@ def test_no_n_plus_one_error
|
|
145
168
|
get :index
|
146
169
|
end
|
147
170
|
end
|
171
|
+
|
172
|
+
```
|
173
|
+
|
174
|
+
As in RSpec, you can use `current_scale` factor instead of `populate` block:
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
def test_no_n_plus_one_error
|
178
|
+
assert_perform_constant_number_of_queries do
|
179
|
+
get :index, params: {per_page: current_scale}
|
180
|
+
end
|
181
|
+
end
|
182
|
+
```
|
183
|
+
|
184
|
+
### With caching
|
185
|
+
|
186
|
+
If you use caching you can face the problem when first request performs more DB queries than others. The solution is:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
# RSpec
|
190
|
+
|
191
|
+
context "N + 1", :n_plus_one do
|
192
|
+
populate { |n| create_list :post, n }
|
193
|
+
|
194
|
+
warmup { get :index } # cache something must be cached
|
195
|
+
|
196
|
+
specify do
|
197
|
+
expect { get :index }.to perform_constant_number_of_queries
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Minitest
|
202
|
+
|
203
|
+
def populate(n)
|
204
|
+
create_list(:post, n)
|
205
|
+
end
|
206
|
+
|
207
|
+
def warmup
|
208
|
+
get :index
|
209
|
+
end
|
210
|
+
|
211
|
+
def test_no_n_plus_one_error
|
212
|
+
assert_perform_constant_number_of_queries do
|
213
|
+
get :index
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# or with params
|
218
|
+
|
219
|
+
def test_no_n_plus_one
|
220
|
+
populate = ->(n) { create_list(:post, n) }
|
221
|
+
warmup = -> { get :index }
|
222
|
+
|
223
|
+
assert_perform_constant_number_of_queries population: populate, warmup: warmup do
|
224
|
+
get :index
|
225
|
+
end
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
229
|
+
If your `warmup` and testing procs are identical, you can use:
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
expext { get :index }.to perform_constant_number_of_queries.with_warming_up # RSpec only
|
148
233
|
```
|
149
234
|
|
150
235
|
### Configuration
|
@@ -160,13 +245,21 @@ NPlusOneControl.default_scale_factors = [2, 3]
|
|
160
245
|
# You can activate verbosity through env variable NPLUSONE_VERBOSE=1
|
161
246
|
NPlusOneControl.verbose = false
|
162
247
|
|
248
|
+
# Print table hits difference, for example:
|
249
|
+
#
|
250
|
+
# Unmatched query numbers by tables:
|
251
|
+
# users (SELECT): 2 != 3
|
252
|
+
# events (INSERT): 1 != 2
|
253
|
+
#
|
254
|
+
self.show_table_stats = true
|
255
|
+
|
163
256
|
# Ignore matching queries
|
164
257
|
NPlusOneControl.ignore = /^(BEGIN|COMMIT|SAVEPOINT|RELEASE)/
|
165
258
|
|
166
259
|
# ActiveSupport notifications event to track queries.
|
167
260
|
# We track ActiveRecord event by default,
|
168
261
|
# but can also track rom-rb events ('sql.rom') as well.
|
169
|
-
NPlusOneControl.event =
|
262
|
+
NPlusOneControl.event = "sql.active_record"
|
170
263
|
|
171
264
|
# configure transactional behavour for populate method
|
172
265
|
# in case of use multiple database connections
|
@@ -180,6 +273,21 @@ NPlusOneControl::Executor.tap do |executor|
|
|
180
273
|
connections.each(&:rollback_transaction)
|
181
274
|
end
|
182
275
|
end
|
276
|
+
|
277
|
+
# Provide a backtrace cleaner callable object used to filter SQL caller location to display in the verbose mode
|
278
|
+
# Set it to nil to disable tracing.
|
279
|
+
#
|
280
|
+
# In Rails apps, we use Rails.backtrace_cleaner by default.
|
281
|
+
NPlusOneControl.backtrace_cleaner = ->(locations_array) { do_some_filtering(locations_array) }
|
282
|
+
|
283
|
+
# You can also specify the number of backtrace lines to show.
|
284
|
+
# MOTE: It could be specified via NPLUSONE_BACKTRACE env var
|
285
|
+
NPlusOneControl.backtrace_length = 1
|
286
|
+
|
287
|
+
# Sometime queries could be too large to provide any meaningful insight.
|
288
|
+
# You can configure an output length limit for quries in verbose mode by setting the follwing option
|
289
|
+
# NOTE: It could be specified via NPLUSONE_TRUNCATE env var
|
290
|
+
NPlusOneControl.truncate_query_size = 100
|
183
291
|
```
|
184
292
|
|
185
293
|
## How does it work?
|
@@ -188,22 +296,29 @@ Take a look at our [Executor](https://github.com/palkan/n_plus_one_control/blob/
|
|
188
296
|
|
189
297
|
## What's next?
|
190
298
|
|
299
|
+
- More matchers.
|
300
|
+
|
191
301
|
It may be useful to provide more matchers/assertions, for example:
|
192
302
|
|
193
303
|
```ruby
|
194
304
|
|
195
305
|
# Actually, that means that it is N+1))
|
196
|
-
assert_linear_number_of_queries {
|
306
|
+
assert_linear_number_of_queries { some_code }
|
197
307
|
|
198
308
|
# But we can tune it with `coef` and handle such cases as selecting in batches
|
199
309
|
assert_linear_number_of_queries(coef: 0.1) do
|
200
|
-
Post.find_in_batches {
|
310
|
+
Post.find_in_batches { some_code }
|
201
311
|
end
|
202
312
|
|
203
313
|
# probably, also make sense to add another curve types
|
204
|
-
assert_logarithmic_number_of_queries {
|
314
|
+
assert_logarithmic_number_of_queries { some_code }
|
205
315
|
```
|
206
316
|
|
317
|
+
- Support custom non-SQL events.
|
318
|
+
|
319
|
+
N+1 problem is not a database specific: we can have N+1 Redis calls, N+1 HTTP external requests, etc.
|
320
|
+
We can make `n_plus_one_control` customizable to support these scenarios (technically, we need to make it possible to handle different payload in the event subscriber).
|
321
|
+
|
207
322
|
If you want to discuss or implement any of these, feel free to open an [issue](https://github.com/palkan/n_plus_one_control/issues) or propose a [pull request](https://github.com/palkan/n_plus_one_control/pulls).
|
208
323
|
|
209
324
|
## Development
|
data/lib/n_plus_one_control.rb
CHANGED
@@ -5,17 +5,94 @@ require "n_plus_one_control/executor"
|
|
5
5
|
|
6
6
|
# RSpec and Minitest matchers to prevent N+1 queries problem.
|
7
7
|
module NPlusOneControl
|
8
|
+
# Used to extract a table name from a query
|
9
|
+
EXTRACT_TABLE_RXP = /(insert into|update|delete from|from) ['"](\S+)['"]/i.freeze
|
10
|
+
|
11
|
+
# Used to convert a query part extracted by the regexp above to the corresponding
|
12
|
+
# human-friendly type
|
13
|
+
QUERY_PART_TO_TYPE = {
|
14
|
+
"insert into" => "INSERT",
|
15
|
+
"update" => "UPDATE",
|
16
|
+
"delete from" => "DELETE",
|
17
|
+
"from" => "SELECT"
|
18
|
+
}.freeze
|
19
|
+
|
8
20
|
class << self
|
9
|
-
attr_accessor :default_scale_factors, :verbose, :ignore, :event
|
21
|
+
attr_accessor :default_scale_factors, :verbose, :show_table_stats, :ignore, :event,
|
22
|
+
:backtrace_cleaner, :backtrace_length, :truncate_query_size
|
10
23
|
|
11
|
-
|
24
|
+
attr_reader :default_matching
|
25
|
+
|
26
|
+
def failure_message(queries) # rubocop:disable Metrics/MethodLength
|
12
27
|
msg = ["Expected to make the same number of queries, but got:\n"]
|
13
28
|
queries.each do |(scale, data)|
|
14
29
|
msg << " #{data.size} for N=#{scale}\n"
|
15
|
-
msg << data.map { |sql| " #{sql}\n" }.join.to_s if verbose
|
16
30
|
end
|
31
|
+
|
32
|
+
msg.concat(table_usage_stats(queries.map(&:last))) if show_table_stats
|
33
|
+
|
34
|
+
if verbose
|
35
|
+
queries.each do |(scale, data)|
|
36
|
+
msg << "Queries for N=#{scale}\n"
|
37
|
+
msg << data.map { |sql| " #{truncate_query(sql)}\n" }.join.to_s
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
17
41
|
msg.join
|
18
42
|
end
|
43
|
+
|
44
|
+
def table_usage_stats(runs) # rubocop:disable Metrics/MethodLength
|
45
|
+
msg = ["Unmatched query numbers by tables:\n"]
|
46
|
+
|
47
|
+
before, after = runs.map do |queries|
|
48
|
+
queries.group_by do |query|
|
49
|
+
matches = query.match(EXTRACT_TABLE_RXP)
|
50
|
+
next unless matches
|
51
|
+
|
52
|
+
" #{matches[2]} (#{QUERY_PART_TO_TYPE[matches[1].downcase]})"
|
53
|
+
end.transform_values(&:count)
|
54
|
+
end
|
55
|
+
|
56
|
+
before.keys.each do |k|
|
57
|
+
next if before[k] == after[k]
|
58
|
+
|
59
|
+
msg << "#{k}: #{before[k]} != #{after[k]}\n"
|
60
|
+
end
|
61
|
+
|
62
|
+
msg
|
63
|
+
end
|
64
|
+
|
65
|
+
def default_matching=(val)
|
66
|
+
unless val
|
67
|
+
@default_matching = nil
|
68
|
+
return
|
69
|
+
end
|
70
|
+
|
71
|
+
@default_matching =
|
72
|
+
if val.is_a?(Regexp)
|
73
|
+
val
|
74
|
+
else
|
75
|
+
Regexp.new(val, Regexp::MULTILINE | Regexp::IGNORECASE)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def truncate_query(sql)
|
82
|
+
return sql unless truncate_query_size
|
83
|
+
|
84
|
+
# Only truncate query, leave tracing (if any) as is
|
85
|
+
parts = sql.split(/(\s+↳)/)
|
86
|
+
|
87
|
+
parts[0] =
|
88
|
+
if truncate_query_size < 4
|
89
|
+
"..."
|
90
|
+
else
|
91
|
+
parts[0][0..(truncate_query_size - 4)] + "..."
|
92
|
+
end
|
93
|
+
|
94
|
+
parts.join
|
95
|
+
end
|
19
96
|
end
|
20
97
|
|
21
98
|
# Scale factors to use.
|
@@ -23,7 +100,10 @@ module NPlusOneControl
|
|
23
100
|
self.default_scale_factors = [2, 3]
|
24
101
|
|
25
102
|
# Print performed queries if true
|
26
|
-
self.verbose = ENV[
|
103
|
+
self.verbose = ENV["NPLUSONE_VERBOSE"] == "1"
|
104
|
+
|
105
|
+
# Print table hits difference
|
106
|
+
self.show_table_stats = true
|
27
107
|
|
28
108
|
# Ignore matching queries
|
29
109
|
self.ignore = /^(BEGIN|COMMIT|SAVEPOINT|RELEASE)/
|
@@ -31,5 +111,16 @@ module NPlusOneControl
|
|
31
111
|
# ActiveSupport notifications event to track queries.
|
32
112
|
# We track ActiveRecord event by default,
|
33
113
|
# but can also track rom-rb events ('sql.rom') as well.
|
34
|
-
self.event =
|
114
|
+
self.event = "sql.active_record"
|
115
|
+
|
116
|
+
# Default query filtering applied if none provided explicitly
|
117
|
+
self.default_matching = ENV["NPLUSONE_FILTER"] || /^SELECT/i
|
118
|
+
|
119
|
+
# Truncate queries in verbose mode to fit the length
|
120
|
+
self.truncate_query_size = ENV["NPLUSONE_TRUNCATE"]&.to_i
|
121
|
+
|
122
|
+
# Define the number of backtrace lines to show
|
123
|
+
self.backtrace_length = ENV.fetch("NPLUSONE_BACKTRACE", 1).to_i
|
35
124
|
end
|
125
|
+
|
126
|
+
require "n_plus_one_control/railtie" if defined?(Rails::Railtie)
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module NPlusOneControl
|
4
4
|
# Runs code for every scale factor
|
5
5
|
# and returns collected queries.
|
6
|
-
|
6
|
+
class Executor
|
7
7
|
# Subscribes to ActiveSupport notifications and collect matching queries.
|
8
8
|
class Collector
|
9
9
|
def initialize(pattern)
|
@@ -19,46 +19,86 @@ module NPlusOneControl
|
|
19
19
|
@queries
|
20
20
|
end
|
21
21
|
|
22
|
-
def callback(_name, _start, _finish, _message_id, values)
|
22
|
+
def callback(_name, _start, _finish, _message_id, values) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/LineLength
|
23
23
|
return if %w[CACHE SCHEMA].include? values[:name]
|
24
|
-
@queries << values[:sql] if @pattern.nil? || (values[:sql] =~ @pattern)
|
25
|
-
end
|
26
|
-
end
|
27
24
|
|
28
|
-
|
29
|
-
attr_accessor :transaction_begin
|
30
|
-
attr_accessor :transaction_rollback
|
25
|
+
return unless @pattern.nil? || (values[:sql] =~ @pattern)
|
31
26
|
|
32
|
-
|
33
|
-
raise ArgumentError, "Block is required!" unless block_given?
|
27
|
+
query = values[:sql]
|
34
28
|
|
35
|
-
|
36
|
-
|
29
|
+
if NPlusOneControl.backtrace_cleaner && NPlusOneControl.verbose
|
30
|
+
source = extract_query_source_location(caller)
|
37
31
|
|
38
|
-
|
39
|
-
with_transaction do
|
40
|
-
population.call(scale)
|
41
|
-
results << [scale, collector.call { yield }]
|
42
|
-
end
|
32
|
+
query = "#{query}\n ↳ #{source.join("\n")}" unless source.empty?
|
43
33
|
end
|
44
|
-
|
34
|
+
|
35
|
+
@queries << query
|
45
36
|
end
|
46
37
|
|
47
38
|
private
|
48
39
|
|
49
|
-
def
|
50
|
-
|
51
|
-
|
52
|
-
ensure
|
53
|
-
transaction_rollback.call
|
40
|
+
def extract_query_source_location(locations)
|
41
|
+
NPlusOneControl.backtrace_cleaner.call(locations.lazy)
|
42
|
+
.take(NPlusOneControl.backtrace_length).to_a
|
54
43
|
end
|
55
44
|
end
|
56
45
|
|
46
|
+
class << self
|
47
|
+
attr_accessor :transaction_begin
|
48
|
+
attr_accessor :transaction_rollback
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :current_scale
|
52
|
+
|
57
53
|
self.transaction_begin = -> do
|
58
54
|
ActiveRecord::Base.connection.begin_transaction(joinable: false)
|
59
55
|
end
|
56
|
+
|
60
57
|
self.transaction_rollback = -> do
|
61
58
|
ActiveRecord::Base.connection.rollback_transaction
|
62
59
|
end
|
60
|
+
|
61
|
+
def initialize(population: nil, scale_factors: nil, matching: nil)
|
62
|
+
@population = population
|
63
|
+
@scale_factors = scale_factors
|
64
|
+
@matching = matching
|
65
|
+
end
|
66
|
+
|
67
|
+
# rubocop:disable Metrics/MethodLength
|
68
|
+
def call
|
69
|
+
raise ArgumentError, "Block is required!" unless block_given?
|
70
|
+
|
71
|
+
results = []
|
72
|
+
collector = Collector.new(matching)
|
73
|
+
|
74
|
+
(scale_factors || NPlusOneControl.default_scale_factors).each do |scale|
|
75
|
+
@current_scale = scale
|
76
|
+
with_transaction do
|
77
|
+
population&.call(scale)
|
78
|
+
results << [scale, collector.call { yield }]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
results
|
82
|
+
end
|
83
|
+
# rubocop:enable Metrics/MethodLength
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def with_transaction
|
88
|
+
transaction_begin.call
|
89
|
+
yield
|
90
|
+
ensure
|
91
|
+
transaction_rollback.call
|
92
|
+
end
|
93
|
+
|
94
|
+
def transaction_begin
|
95
|
+
self.class.transaction_begin
|
96
|
+
end
|
97
|
+
|
98
|
+
def transaction_rollback
|
99
|
+
self.class.transaction_rollback
|
100
|
+
end
|
101
|
+
|
102
|
+
attr_reader :population, :scale_factors, :matching
|
63
103
|
end
|
64
104
|
end
|