activerecord-transactionable 0.1.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 +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +7 -0
- data/README.md +48 -0
- data/Rakefile +6 -0
- data/activerecord-transactionable.gemspec +28 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/activerecord/transactionable.rb +83 -0
- data/lib/activerecord/transactionable/version.rb +5 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 134ddc5a2d594c0ddf785ff45ff5ec2661f0660e
|
4
|
+
data.tar.gz: 3fb2afbaa552fbec81c70bb7824874646efacfde
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 78d23d187b037feb977342e6120d001c92699b7db37dfd4cb953bd4add4dcc91762f822c359ce841dd3725d6c6232fcfe375d4b2f99f61415a9fc9aabff9e5a7
|
7
|
+
data.tar.gz: 71d8ae5296a058dca0ff90c746b15320824556c6fde806abcdf3e01e3556e7b8a4b1e09e438bec82473222c3b60631525e9b67632f275317122055b872b49590
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# So the gem can run the simple test suite against the raw bundled gems without the complex BUNDLE_GEMFILE setup
|
4
|
+
gem "sqlite3", :platforms => [:ruby]
|
5
|
+
|
6
|
+
# Specify your gem's dependencies in activerecord-transactionable.gemspec
|
7
|
+
gemspec
|
data/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Activerecord::Transactionable
|
2
|
+
|
3
|
+
Provides a method, `transaction_wrapper` at the class and instance levels that can be used instead of `ActiveRecord#transaction`.
|
4
|
+
|
5
|
+
Useful as an example of correct behavior for wrapping transactions.
|
6
|
+
|
7
|
+
NOTE: Rails' transactions are per-database connection, not per-model, nor per-instance,
|
8
|
+
see: http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'activerecord-transactionable'
|
16
|
+
```
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install activerecord-transactionable
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
When creating, saving, deleting within the transaction make sure to use the bang methods (`!`) in order to ensure a rollback on failure.
|
29
|
+
|
30
|
+
```
|
31
|
+
car = Car.new(name: "Fiesta")
|
32
|
+
car.transaction_wrapper do
|
33
|
+
car.save!
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
Also see the specs.
|
38
|
+
|
39
|
+
## Development
|
40
|
+
|
41
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
42
|
+
|
43
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
44
|
+
|
45
|
+
## Contributing
|
46
|
+
|
47
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/pboling/activerecord-transactionable.
|
48
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'activerecord/transactionable/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "activerecord-transactionable"
|
8
|
+
spec.version = Activerecord::Transactionable::VERSION
|
9
|
+
spec.authors = ["Peter Boling"]
|
10
|
+
spec.email = ["peter.boling@gmail.com"]
|
11
|
+
spec.licenses = ["MIT"]
|
12
|
+
|
13
|
+
spec.summary = %q{Do ActiveRecord transactions the right way.}
|
14
|
+
spec.description = %q{Getting transactions right is hard, and this gem makes it easier.}
|
15
|
+
spec.homepage = "http://www.railsbling.com/tags/activerecord-transactionable"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency "activemodel"
|
23
|
+
spec.add_dependency "activerecord"
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.4"
|
28
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "activerecord/transactionable"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require "activerecord/transactionable/version"
|
2
|
+
require "active_model"
|
3
|
+
require "active_record"
|
4
|
+
|
5
|
+
module Activerecord
|
6
|
+
# SRP: Provides an example of correct behavior for wrapping transactions.
|
7
|
+
# NOTE: Rails' transactions are per-database connection, not per-model, nor per-instance,
|
8
|
+
# see: http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
|
9
|
+
module Transactionable
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
DEFAULT_ERRORS_TO_HANDLE = [ActiveRecord::RecordInvalid]
|
13
|
+
DEFAULT_ERRORS_WHICH_PREPARE_ERRORS_ON_SELF = [ActiveRecord::RecordInvalid]
|
14
|
+
|
15
|
+
def transaction_wrapper(**args)
|
16
|
+
self.class.transaction_wrapper(object: self, **args) do
|
17
|
+
yield
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
def transaction_wrapper(object: nil, **args)
|
23
|
+
rescued_errors = Array(args[:rescued_errors])
|
24
|
+
prepared_errors = Array(args[:prepared_errors])
|
25
|
+
retriable_errors = Array(args[:retriable_errors])
|
26
|
+
reraisable_errors = Array(args[:reraisable_errors])
|
27
|
+
rescued_errors.concat(DEFAULT_ERRORS_TO_HANDLE)
|
28
|
+
prepared_errors.concat(DEFAULT_ERRORS_WHICH_PREPARE_ERRORS_ON_SELF)
|
29
|
+
already_been_added_to_self, needing_added_to_self = rescued_errors.partition {|error_class| prepared_errors.include?(error_class)}
|
30
|
+
re_try = false
|
31
|
+
begin
|
32
|
+
ActiveRecord::Base.transaction do
|
33
|
+
yield
|
34
|
+
end
|
35
|
+
true # <= make the return value meaningful. Meaning is either: transaction succeeded, OR raised ActiveRecord::Rollback
|
36
|
+
rescue *reraisable_errors => error
|
37
|
+
# This has highest precedence because raising is the most critical functionality of a raised error to keep
|
38
|
+
# if that is in the intended behavior, and this way a specific child of StandardError can be reraised while
|
39
|
+
# the parent can still be caught and added to self.errors
|
40
|
+
# Also adds the error to the object if there is an object.
|
41
|
+
transaction_error_logger(object: object, error: error, add_to: nil, additional_message: " [re-raising!]")
|
42
|
+
raise error
|
43
|
+
rescue *retriable_errors => error
|
44
|
+
# This will re-run the begin block above
|
45
|
+
# WARNING: If the same error keeps getting thrown this would infinitely recurse!
|
46
|
+
# To avoid the infinite recursion, we track the retry state
|
47
|
+
if re_try
|
48
|
+
transaction_error_logger(object: object, error: error, additional_message: " [2nd attempt]")
|
49
|
+
false # <= make the return value meaningful. Meaning is: transaction failed after two attempts
|
50
|
+
else
|
51
|
+
re_try = true
|
52
|
+
# Not adding error to base when retrying, because otherwise the added error may
|
53
|
+
# prevent the subsequent save from working, in a catch-22
|
54
|
+
transaction_error_logger(object: object, error: error, add_to: nil, additional_message: " [1st attempt]")
|
55
|
+
retry
|
56
|
+
end
|
57
|
+
rescue *already_been_added_to_self => error
|
58
|
+
# ActiveRecord::RecordInvalid, when done correctly, will have already added the error to object.
|
59
|
+
transaction_error_logger(object: nil, error: error, additional_message: nil)
|
60
|
+
false # <= make the return value meaningful. Meaning is: transaction failed
|
61
|
+
rescue *needing_added_to_self => error
|
62
|
+
transaction_error_logger(object: object, error: error, additional_message: nil)
|
63
|
+
false # <= make the return value meaningful. Meaning is: transaction failed
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def transaction_error_logger(object:, error:, add_to: :base, additional_message: nil)
|
68
|
+
# Ruby arguments, like object, are passed by reference,
|
69
|
+
# so this update to errors will be available to the caller
|
70
|
+
if object.nil?
|
71
|
+
# when a transaction wraps a bunch of CRUD actions,
|
72
|
+
# the specific record that caused the ActiveRecord::RecordInvalid error may be out of scope
|
73
|
+
# Ideally you would rewrite the caller to call transaction_wrapper on a single record (even if updates happen on other records)
|
74
|
+
logger.error("[#{self}.transaction_wrapper] #{error.class}: #{error.message}#{additional_message}")
|
75
|
+
else
|
76
|
+
logger.error("[#{self}.transaction_wrapper] On #{object.class} #{error.class}: #{error.message}#{additional_message}")
|
77
|
+
object.errors.add(add_to, error.message) unless add_to.nil?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-transactionable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Peter Boling
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-03-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activemodel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activerecord
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.11'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.11'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.4'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.4'
|
83
|
+
description: Getting transactions right is hard, and this gem makes it easier.
|
84
|
+
email:
|
85
|
+
- peter.boling@gmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- ".rspec"
|
92
|
+
- ".travis.yml"
|
93
|
+
- Gemfile
|
94
|
+
- README.md
|
95
|
+
- Rakefile
|
96
|
+
- activerecord-transactionable.gemspec
|
97
|
+
- bin/console
|
98
|
+
- bin/setup
|
99
|
+
- lib/activerecord/transactionable.rb
|
100
|
+
- lib/activerecord/transactionable/version.rb
|
101
|
+
homepage: http://www.railsbling.com/tags/activerecord-transactionable
|
102
|
+
licenses:
|
103
|
+
- MIT
|
104
|
+
metadata: {}
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
require_paths:
|
108
|
+
- lib
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
requirements: []
|
120
|
+
rubyforge_project:
|
121
|
+
rubygems_version: 2.4.8
|
122
|
+
signing_key:
|
123
|
+
specification_version: 4
|
124
|
+
summary: Do ActiveRecord transactions the right way.
|
125
|
+
test_files: []
|