deferring 0.0.1
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/.gitignore +18 -0
- data/.rspec +2 -0
- data/Appraisals +15 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +236 -0
- data/Rakefile +11 -0
- data/deferring.gemspec +31 -0
- data/gemfiles/rails_30.gemfile +7 -0
- data/gemfiles/rails_30.gemfile.lock +51 -0
- data/gemfiles/rails_32.gemfile +7 -0
- data/gemfiles/rails_32.gemfile.lock +53 -0
- data/gemfiles/rails_4.gemfile +7 -0
- data/gemfiles/rails_4.gemfile.lock +58 -0
- data/gemfiles/rails_40.gemfile +7 -0
- data/gemfiles/rails_40.gemfile.lock +59 -0
- data/gemfiles/rails_41.gemfile +7 -0
- data/gemfiles/rails_41.gemfile.lock +58 -0
- data/lib/deferring.rb +150 -0
- data/lib/deferring/deferred_association.rb +187 -0
- data/lib/deferring/version.rb +5 -0
- data/spec/lib/deferring_spec.rb +536 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/active_record.rb +28 -0
- data/spec/support/models.rb +67 -0
- data/spec/support/rails_versions.rb +13 -0
- metadata +175 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Appraisals
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
appraise 'rails-30' do
|
2
|
+
gem 'activerecord', '3.0.19'
|
3
|
+
end
|
4
|
+
|
5
|
+
appraise 'rails-32' do
|
6
|
+
gem 'activerecord', '3.2.17'
|
7
|
+
end
|
8
|
+
|
9
|
+
appraise 'rails-40' do
|
10
|
+
gem 'activerecord', '4.0.4'
|
11
|
+
end
|
12
|
+
|
13
|
+
appraise 'rails-41' do
|
14
|
+
gem 'activerecord', '4.1.0'
|
15
|
+
end
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Robin Roestenburg
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
# Deferring
|
2
|
+
|
3
|
+
Deferring makes it possible to delay saving ActiveRecord associations until the
|
4
|
+
parent object has been saved.
|
5
|
+
|
6
|
+
Currently supporting Rails 3.0, 3.2, 4.0 and 4.1 on Ruby 1.9.3.
|
7
|
+
|
8
|
+
It is important to note that Deferring does not touch the original `has_many`
|
9
|
+
and `has_and_belongs_to_many` associations. You can use them, without worrying
|
10
|
+
about any changed behaviour or side-effects from using Deferring.
|
11
|
+
|
12
|
+
**NOTE: This is currently work in progress.**
|
13
|
+
|
14
|
+
## Why use it?
|
15
|
+
|
16
|
+
Let's take a look at the following example:
|
17
|
+
|
18
|
+
``` ruby
|
19
|
+
class Person
|
20
|
+
has_and_belongs_to_many :teams
|
21
|
+
validates :name, presence: true
|
22
|
+
end
|
23
|
+
|
24
|
+
class Team
|
25
|
+
has_and_belongs_to_many :people
|
26
|
+
end
|
27
|
+
|
28
|
+
support = Team.create(name: 'Support')
|
29
|
+
person = Person.create(name: 'Bob')
|
30
|
+
|
31
|
+
person.teams << support
|
32
|
+
person.name = nil
|
33
|
+
person.save
|
34
|
+
# => false, because the name attribute is empty
|
35
|
+
|
36
|
+
Person.first.teams
|
37
|
+
# => [#<Team id: 4, name: "Support", ... ]
|
38
|
+
```
|
39
|
+
|
40
|
+
The links to the Teams associated to the Person are stored directly, before the
|
41
|
+
(in this case invalid) parent is actually saved. This is how Rails' `has_many`
|
42
|
+
and `has_and_belongs_to_many` associations work, but not how (imho) they should
|
43
|
+
work in this situation.
|
44
|
+
|
45
|
+
The `deferring` gem will delay creating the links between Person and Team until
|
46
|
+
the Person has been saved successfully. Let's look at the example again, only
|
47
|
+
now using the `deferring` gem:
|
48
|
+
|
49
|
+
``` ruby
|
50
|
+
class Person
|
51
|
+
deferred_has_and_belongs_to_many :teams
|
52
|
+
validates :name, presence: true
|
53
|
+
end
|
54
|
+
|
55
|
+
class Team
|
56
|
+
has_and_belongs_to_many :people
|
57
|
+
end
|
58
|
+
support = Team.create(name: 'Support')
|
59
|
+
person = Person.create(name: 'Bob')
|
60
|
+
|
61
|
+
person.teams << support
|
62
|
+
person.name = nil
|
63
|
+
person.save
|
64
|
+
# => false, because the name attribute is empty
|
65
|
+
|
66
|
+
Person.first.teams
|
67
|
+
# => []
|
68
|
+
```
|
69
|
+
|
70
|
+
|
71
|
+
## Use cases
|
72
|
+
|
73
|
+
* Auditing
|
74
|
+
* ...
|
75
|
+
|
76
|
+
|
77
|
+
## Credits/Rationale
|
78
|
+
|
79
|
+
The idea for this gem was originally thought of by Tyler Rick (see [this Ruby
|
80
|
+
form thread from 2006](https://www.ruby-forum.com/topic/81095)). The gem created
|
81
|
+
by TylerRick is still
|
82
|
+
[available](https://github.com/TylerRick/has_and_belongs_to_many_with_deferred_save),
|
83
|
+
but unmaintained. This gem has been forked by Martin Koerner, who released his
|
84
|
+
fork as a gem called
|
85
|
+
[`deferred_associations`](https://rubygems.org/gems/deferred_associations).
|
86
|
+
Koerner fixes some issues with Rick's original implementation and added support
|
87
|
+
for Rails 3 and 4.
|
88
|
+
|
89
|
+
A project I am working on, uses the
|
90
|
+
[`autosave_habtm`](https://rubygems.org/gems/autosave_habtm) gem, which kind of
|
91
|
+
takes different approach to doing the same thing. This gem only supports Rails
|
92
|
+
3.0.
|
93
|
+
|
94
|
+
As we are upgrading to Rails 3.2 (and later Rails 4), I needed a gem to provide
|
95
|
+
this behaviour. Upgrading either one of the gems would result into rewriting a
|
96
|
+
lot of the code (for different reasons, some purely esthetic :)), so that is why
|
97
|
+
I wrote a new gem.
|
98
|
+
|
99
|
+
|
100
|
+
## Getting started
|
101
|
+
|
102
|
+
### Installation
|
103
|
+
|
104
|
+
Add this line to your application's Gemfile:
|
105
|
+
|
106
|
+
gem 'deferring'
|
107
|
+
|
108
|
+
And then execute:
|
109
|
+
|
110
|
+
$ bundle
|
111
|
+
|
112
|
+
Or install it yourself as:
|
113
|
+
|
114
|
+
$ gem install deferring
|
115
|
+
|
116
|
+
|
117
|
+
### How do I use it?
|
118
|
+
|
119
|
+
Deferring adds a couple of methods to your ActiveRecord models. These are:
|
120
|
+
|
121
|
+
- `deferred_has_and_belongs_to_many`
|
122
|
+
- `deferred_accepts_nested_attributes_for`
|
123
|
+
- `deferred_has_many`
|
124
|
+
|
125
|
+
These methods wrap the existing methods. For instance, `deferred_has_many` will
|
126
|
+
call `has_many` in order to set up the association.
|
127
|
+
|
128
|
+
**TODO:** Describe pending_creates/pending_deletes/links/unlinks/callbacks/
|
129
|
+
original_name/checked.
|
130
|
+
|
131
|
+
|
132
|
+
### How does it work?
|
133
|
+
|
134
|
+
Deferring wraps the original ActiveRecord association and replaces the accessor
|
135
|
+
methods to the association by a custom object that will keep track of the
|
136
|
+
updates to the association. This wrapper is basically an array with some extras
|
137
|
+
to match the ActiveRecord API.
|
138
|
+
|
139
|
+
When the parent is saved, this object is assigned to the original association
|
140
|
+
(using an `after_save` callback on the parent model) which will automatically
|
141
|
+
save the changes to the database.
|
142
|
+
|
143
|
+
For the astute reader: Yes, the gem abuses the exact problem it is trying to
|
144
|
+
avoid ;-)
|
145
|
+
|
146
|
+
### Gotchas
|
147
|
+
|
148
|
+
#### Using autosave (or not actually)
|
149
|
+
|
150
|
+
TL;DR; Using `autosave: true` (or false) on a deferred association will work,
|
151
|
+
but does not do anything.
|
152
|
+
|
153
|
+
This is what the Rails documentation says about the AutosaveAssociation:
|
154
|
+
|
155
|
+
_AutosaveAssociation is a module that takes care of automatically saving
|
156
|
+
associated records when their parent is saved. In addition to saving, it also
|
157
|
+
destroys any associated records that were marked for destruction._
|
158
|
+
|
159
|
+
_If validations for any of the associations fail, their error messages will be
|
160
|
+
applied to the parent._
|
161
|
+
|
162
|
+
The `deferring` gem works with `pending_deletes` (or the alias `unlinks`)
|
163
|
+
instead of the `marked_for_destruction` flag, so everything related to that in
|
164
|
+
AutosaveAssociation does not work as you would expect.
|
165
|
+
|
166
|
+
Also, `deferring` adds the associated records present in a deferred
|
167
|
+
association to the original (in this case, autosaved) association by assigning
|
168
|
+
the array of associated records to original association. This kind of assignment
|
169
|
+
bypasses the autosave behaviour, see the _Why use it?_ part on top of this
|
170
|
+
README.
|
171
|
+
|
172
|
+
#### Using custom callback methods
|
173
|
+
|
174
|
+
**TODO**: This is incorrect and has to be rewritten to match code.
|
175
|
+
|
176
|
+
You can use custom callback functions. However, the callbacks for defferred
|
177
|
+
associations are triggered at a different point in time.
|
178
|
+
|
179
|
+
An example to illustrate:
|
180
|
+
|
181
|
+
``` ruby
|
182
|
+
class Person < ActiveRecord::Base
|
183
|
+
has_and_belongs_to_many :teams, before_add: :before_adding
|
184
|
+
deferred_has_and_belongs_to_many :pets, before_add: :before_adding
|
185
|
+
|
186
|
+
def audit_log
|
187
|
+
@log = []
|
188
|
+
end
|
189
|
+
|
190
|
+
def before_adding(record)
|
191
|
+
audit_log << "Before adding #{record.class} with id #{record.id}"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
196
|
+
This sets up a Person model that has a regular HABTM association with teams and
|
197
|
+
that has a deferred HABTM association with pets. Each time a team or pet is
|
198
|
+
added to the database a log statement is written to the audit log (using the
|
199
|
+
`before_adding` callback function).
|
200
|
+
|
201
|
+
The regular HABTM association behaves likes this:
|
202
|
+
|
203
|
+
``` ruby
|
204
|
+
person = Person.first
|
205
|
+
person.teams << Team.find(1)
|
206
|
+
person.audit_log # => ['Before adding Team 1']
|
207
|
+
```
|
208
|
+
|
209
|
+
As records of deferred associations are saved to the database after saving the
|
210
|
+
parent the behavior is a bit different:
|
211
|
+
|
212
|
+
``` ruby
|
213
|
+
person = Person.first
|
214
|
+
person.pets << Pet.find(1)
|
215
|
+
person.audit_log # => []
|
216
|
+
|
217
|
+
person.save
|
218
|
+
person.audit_log # => ['Before adding Pet 1']
|
219
|
+
```
|
220
|
+
|
221
|
+
## TODO
|
222
|
+
|
223
|
+
* add support for more Rubies
|
224
|
+
* check out what is going on with uniq: true
|
225
|
+
* collection(true) (same as reload)
|
226
|
+
* collection.replace
|
227
|
+
* validations!
|
228
|
+
* validate: false does not work
|
229
|
+
|
230
|
+
## Contributing
|
231
|
+
|
232
|
+
1. Fork it
|
233
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
234
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
235
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
236
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/deferring.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'deferring/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'deferring'
|
8
|
+
spec.version = Deferring::VERSION
|
9
|
+
spec.authors = ['Robin Roestenburg']
|
10
|
+
spec.email = ['robin@roestenburg.io']
|
11
|
+
spec.description = %q{
|
12
|
+
The Deferring gem makes it possible to defer saving ActiveRecord
|
13
|
+
associations until the parent object is saved.
|
14
|
+
}
|
15
|
+
spec.summary = %q{Defer saving ActiveRecord associations until parent is saved}
|
16
|
+
spec.homepage = 'http://github.com/robinroestenburg/delay_many'
|
17
|
+
spec.license = "MIT"
|
18
|
+
|
19
|
+
spec.files = `git ls-files`.split($/)
|
20
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
21
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
22
|
+
spec.require_paths = ['lib']
|
23
|
+
|
24
|
+
spec.add_dependency 'activerecord', '> 3.0'
|
25
|
+
|
26
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
27
|
+
spec.add_development_dependency 'rake'
|
28
|
+
spec.add_development_dependency 'rspec'
|
29
|
+
spec.add_development_dependency 'sqlite3'
|
30
|
+
spec.add_development_dependency 'appraisal'
|
31
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
deferring (0.0.1)
|
5
|
+
activerecord (> 3.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activemodel (3.0.19)
|
11
|
+
activesupport (= 3.0.19)
|
12
|
+
builder (~> 2.1.2)
|
13
|
+
i18n (~> 0.5.0)
|
14
|
+
activerecord (3.0.19)
|
15
|
+
activemodel (= 3.0.19)
|
16
|
+
activesupport (= 3.0.19)
|
17
|
+
arel (~> 2.0.10)
|
18
|
+
tzinfo (~> 0.3.23)
|
19
|
+
activesupport (3.0.19)
|
20
|
+
appraisal (1.0.0)
|
21
|
+
bundler
|
22
|
+
rake
|
23
|
+
thor (>= 0.14.0)
|
24
|
+
arel (2.0.10)
|
25
|
+
builder (2.1.2)
|
26
|
+
diff-lcs (1.2.5)
|
27
|
+
i18n (0.5.2)
|
28
|
+
rake (10.3.1)
|
29
|
+
rspec (2.14.1)
|
30
|
+
rspec-core (~> 2.14.0)
|
31
|
+
rspec-expectations (~> 2.14.0)
|
32
|
+
rspec-mocks (~> 2.14.0)
|
33
|
+
rspec-core (2.14.8)
|
34
|
+
rspec-expectations (2.14.5)
|
35
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
36
|
+
rspec-mocks (2.14.6)
|
37
|
+
sqlite3 (1.3.9)
|
38
|
+
thor (0.19.1)
|
39
|
+
tzinfo (0.3.39)
|
40
|
+
|
41
|
+
PLATFORMS
|
42
|
+
ruby
|
43
|
+
|
44
|
+
DEPENDENCIES
|
45
|
+
activerecord (= 3.0.19)
|
46
|
+
appraisal
|
47
|
+
bundler (~> 1.3)
|
48
|
+
deferring!
|
49
|
+
rake
|
50
|
+
rspec
|
51
|
+
sqlite3
|
@@ -0,0 +1,53 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
deferring (0.0.1)
|
5
|
+
activerecord (> 3.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activemodel (3.2.17)
|
11
|
+
activesupport (= 3.2.17)
|
12
|
+
builder (~> 3.0.0)
|
13
|
+
activerecord (3.2.17)
|
14
|
+
activemodel (= 3.2.17)
|
15
|
+
activesupport (= 3.2.17)
|
16
|
+
arel (~> 3.0.2)
|
17
|
+
tzinfo (~> 0.3.29)
|
18
|
+
activesupport (3.2.17)
|
19
|
+
i18n (~> 0.6, >= 0.6.4)
|
20
|
+
multi_json (~> 1.0)
|
21
|
+
appraisal (1.0.0)
|
22
|
+
bundler
|
23
|
+
rake
|
24
|
+
thor (>= 0.14.0)
|
25
|
+
arel (3.0.3)
|
26
|
+
builder (3.0.4)
|
27
|
+
diff-lcs (1.2.5)
|
28
|
+
i18n (0.6.9)
|
29
|
+
multi_json (1.9.2)
|
30
|
+
rake (10.3.1)
|
31
|
+
rspec (2.14.1)
|
32
|
+
rspec-core (~> 2.14.0)
|
33
|
+
rspec-expectations (~> 2.14.0)
|
34
|
+
rspec-mocks (~> 2.14.0)
|
35
|
+
rspec-core (2.14.8)
|
36
|
+
rspec-expectations (2.14.5)
|
37
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
38
|
+
rspec-mocks (2.14.6)
|
39
|
+
sqlite3 (1.3.9)
|
40
|
+
thor (0.19.1)
|
41
|
+
tzinfo (0.3.39)
|
42
|
+
|
43
|
+
PLATFORMS
|
44
|
+
ruby
|
45
|
+
|
46
|
+
DEPENDENCIES
|
47
|
+
activerecord (= 3.2.17)
|
48
|
+
appraisal
|
49
|
+
bundler (~> 1.3)
|
50
|
+
deferring!
|
51
|
+
rake
|
52
|
+
rspec
|
53
|
+
sqlite3
|