deferred_associations 0.5.4 → 0.5.5
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/CHANGELOG +5 -0
- data/Rakefile +27 -26
- data/Readme.markdown +85 -82
- data/VERSION +1 -1
- data/deferred_associations.gemspec +61 -75
- data/init.rb +1 -1
- data/lib/array_to_association_wrapper.rb +0 -0
- data/lib/deferred_associations.rb +0 -0
- data/lib/has_and_belongs_to_many_with_deferred_save.rb +142 -142
- data/lib/has_many_with_deferred_save.rb +10 -1
- data/spec/db/database.yml +21 -21
- data/spec/db/schema.rb +40 -40
- data/spec/has_and_belongs_to_many_with_deferred_save_spec.rb +236 -234
- data/spec/has_many_with_deferred_save_spec.rb +28 -1
- data/spec/models/chair.rb +0 -0
- data/spec/models/door.rb +3 -3
- data/spec/models/person.rb +15 -15
- data/spec/models/room.rb +51 -51
- data/spec/models/table.rb +0 -0
- data/spec/spec_helper.rb +47 -39
- metadata +85 -95
- data/.gitignore +0 -3
- data/.travis.yml +0 -12
- data/gemfiles/ar2.3.14.gemfile +0 -12
- data/gemfiles/ar2.3.14.gemfile.lock +0 -47
- data/gemfiles/ar3.2.3.gemfile +0 -12
- data/gemfiles/ar3.2.3.gemfile.lock +0 -96
- data/spec/.gitignore +0 -2
data/CHANGELOG
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
0.5.5
|
2
|
+
=====
|
3
|
+
* id-setter for has_many associations couldn't be used twice on the same object
|
4
|
+
* id-getter for has_many associations always returned the saved IDs, even if a new array was set
|
5
|
+
|
1
6
|
0.5.0
|
2
7
|
=====
|
3
8
|
* Added has_many with deferred save, which works like habtm with deferred save
|
data/Rakefile
CHANGED
@@ -1,26 +1,27 @@
|
|
1
|
-
task :default do |t|
|
2
|
-
options = "--colour"
|
3
|
-
files = FileList['spec/**/*_spec.rb'].map{|f| f.sub(%r{^spec/},'') }
|
4
|
-
exit system("cd spec && spec #{options} #{files}") ? 0 : 1
|
5
|
-
end
|
6
|
-
|
7
|
-
begin
|
8
|
-
require 'jeweler'
|
9
|
-
project_name = 'deferred_associations'
|
10
|
-
Jeweler::Tasks.new do |gem|
|
11
|
-
gem.name = project_name
|
12
|
-
gem.summary = "Makes ActiveRecord defer/postpone habtm or has_many associations"
|
13
|
-
gem.description = "Makes ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) or has_many
|
14
|
-
association until you call model.save, allowing validation in the style of normal attributes. Additionally you
|
15
|
-
can check inside before_save filters, if the association was altered."
|
16
|
-
gem.homepage = "http://github.com/MartinKoerner/deferred_associations"
|
17
|
-
gem.email = "martin.koerner@objectfab.de"
|
18
|
-
gem.authors = ["Martin Koerner", "Tyler Rick", "Alessio Caiazza"]
|
19
|
-
gem.add_dependency('activerecord')
|
20
|
-
gem.add_development_dependency('rspec')
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
1
|
+
task :default do |t|
|
2
|
+
options = "--colour"
|
3
|
+
files = FileList['spec/**/*_spec.rb'].map{|f| f.sub(%r{^spec/},'') }
|
4
|
+
exit system("cd spec && spec #{options} #{files}") ? 0 : 1
|
5
|
+
end
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'jeweler'
|
9
|
+
project_name = 'deferred_associations'
|
10
|
+
Jeweler::Tasks.new do |gem|
|
11
|
+
gem.name = project_name
|
12
|
+
gem.summary = "Makes ActiveRecord defer/postpone habtm or has_many associations"
|
13
|
+
gem.description = "Makes ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) or has_many
|
14
|
+
association until you call model.save, allowing validation in the style of normal attributes. Additionally you
|
15
|
+
can check inside before_save filters, if the association was altered."
|
16
|
+
gem.homepage = "http://github.com/MartinKoerner/deferred_associations"
|
17
|
+
gem.email = "martin.koerner@objectfab.de"
|
18
|
+
gem.authors = ["Martin Koerner", "Tyler Rick", "Alessio Caiazza"]
|
19
|
+
gem.add_dependency('activerecord')
|
20
|
+
gem.add_development_dependency('rspec')
|
21
|
+
gem.files.exclude 'gemfiles/*', '.travis.yml'
|
22
|
+
end
|
23
|
+
|
24
|
+
Jeweler::GemcutterTasks.new
|
25
|
+
rescue LoadError
|
26
|
+
puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
27
|
+
end
|
data/Readme.markdown
CHANGED
@@ -1,82 +1,85 @@
|
|
1
|
-
Make ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) or has_many association
|
2
|
-
until you call model.save, allowing validation in the style of normal attributes.
|
3
|
-
|
4
|
-
[](http://travis-ci.org/MartinKoerner/deferred_associations) [](https://gemnasium.com/MartinKoerner/deferred_associations)
|
5
|
-
|
6
|
-
How to install
|
7
|
-
==============
|
8
|
-
|
9
|
-
gem install deferred_associations
|
10
|
-
|
11
|
-
Usage
|
12
|
-
=====
|
13
|
-
|
14
|
-
class Room < ActiveRecord::Base
|
15
|
-
has_and_belongs_to_many_with_deferred_save :people
|
16
|
-
has_many_with_deferred_save :tables
|
17
|
-
|
18
|
-
validate :usage
|
19
|
-
before_save :check_change
|
20
|
-
|
21
|
-
def usage
|
22
|
-
if people.size > 30
|
23
|
-
errors.add :people, "There are too many people in this room"
|
24
|
-
end
|
25
|
-
if tables.size > 15
|
26
|
-
errors.add :tables, "There are too many tables in this room"
|
27
|
-
end
|
28
|
-
# Neither people nor tables are saved to the database, if a validation error is added
|
29
|
-
end
|
30
|
-
|
31
|
-
def check_change
|
32
|
-
# you can check, if there were changes to the association
|
33
|
-
if people != people_without_deferred_save
|
34
|
-
self.updated_at = Time.now.utc
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
Compatibility
|
40
|
-
=============
|
41
|
-
|
42
|
-
Tested with Rails 2.3.14, 3.2.3 on Ruby 1.8.7, 1.9.3 and JRuby 1.
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
room
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
1
|
+
Make ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) or has_many association
|
2
|
+
until you call model.save, allowing validation in the style of normal attributes.
|
3
|
+
|
4
|
+
[](http://travis-ci.org/MartinKoerner/deferred_associations) [](https://gemnasium.com/MartinKoerner/deferred_associations)
|
5
|
+
|
6
|
+
How to install
|
7
|
+
==============
|
8
|
+
|
9
|
+
gem install deferred_associations
|
10
|
+
|
11
|
+
Usage
|
12
|
+
=====
|
13
|
+
|
14
|
+
class Room < ActiveRecord::Base
|
15
|
+
has_and_belongs_to_many_with_deferred_save :people
|
16
|
+
has_many_with_deferred_save :tables
|
17
|
+
|
18
|
+
validate :usage
|
19
|
+
before_save :check_change
|
20
|
+
|
21
|
+
def usage
|
22
|
+
if people.size > 30
|
23
|
+
errors.add :people, "There are too many people in this room"
|
24
|
+
end
|
25
|
+
if tables.size > 15
|
26
|
+
errors.add :tables, "There are too many tables in this room"
|
27
|
+
end
|
28
|
+
# Neither people nor tables are saved to the database, if a validation error is added
|
29
|
+
end
|
30
|
+
|
31
|
+
def check_change
|
32
|
+
# you can check, if there were changes to the association
|
33
|
+
if people != people_without_deferred_save
|
34
|
+
self.updated_at = Time.now.utc
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
Compatibility
|
40
|
+
=============
|
41
|
+
|
42
|
+
Tested with Rails 2.3.14, 3.2.3, 3.2.14 on Ruby 1.8.7, 1.9.3 and JRuby 1.7.4
|
43
|
+
|
44
|
+
Note, that Rails 3.2.14 associations are partly broken under JRuby cause of https://github.com/rails/rails/issues/11595
|
45
|
+
You'll need to upgrade activerecord-jdbc-adapter to >= 1.3.0.beta1, if you want to use this combination.
|
46
|
+
|
47
|
+
Gotchas
|
48
|
+
=======
|
49
|
+
|
50
|
+
Be aware, that the habtm association objects sometimes asks the database instead of giving you the data directly from the array. So you can get something
|
51
|
+
like
|
52
|
+
|
53
|
+
room = Room.create
|
54
|
+
room.people << Person.create
|
55
|
+
room.people.first # => nil, since the DB doesn't have the association saved yet
|
56
|
+
|
57
|
+
|
58
|
+
Also it is good to know, that the array you set to an association is stored there directly, so after setting a list, the typical association
|
59
|
+
methods are not working:
|
60
|
+
|
61
|
+
room = Room.create
|
62
|
+
room.people.klass # => Person
|
63
|
+
room.people = [Person.first]
|
64
|
+
room.people.klass # => undefined method klass for #Array:0x007fa3b9efc2c0`
|
65
|
+
|
66
|
+
Bugs
|
67
|
+
====
|
68
|
+
|
69
|
+
http://github.com/MartinKoerner/deferred_associations/issues
|
70
|
+
|
71
|
+
History
|
72
|
+
======
|
73
|
+
|
74
|
+
Most of the code for the habtm association was written by TylerRick for his gem [has_and_belongs_to_many_with_deferred_save](https://github.com/TylerRick/has_and_belongs_to_many_with_deferred_save)
|
75
|
+
Mainly, I changed two things:
|
76
|
+
|
77
|
+
* added ActiveRecord 3 compatibility
|
78
|
+
* removed singleton methods, because they interfere with caching
|
79
|
+
|
80
|
+
License
|
81
|
+
=======
|
82
|
+
|
83
|
+
This plugin is licensed under the BSD license.
|
84
|
+
|
85
|
+
2013 (c) Martin Körner
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.5.
|
1
|
+
0.5.5
|
@@ -1,75 +1,61 @@
|
|
1
|
-
# Generated by jeweler
|
2
|
-
# DO NOT EDIT THIS FILE
|
3
|
-
# Instead, edit Jeweler::Tasks in
|
4
|
-
# -*- encoding: utf-8 -*-
|
5
|
-
|
6
|
-
Gem::Specification.new do |s|
|
7
|
-
s.name = "deferred_associations"
|
8
|
-
s.version = "0.5.
|
9
|
-
|
10
|
-
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
-
s.authors = ["Martin Koerner", "Tyler Rick", "Alessio Caiazza"]
|
12
|
-
s.date = "
|
13
|
-
s.description = "Makes ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) or has_many\n association until you call model.save, allowing validation in the style of normal attributes. Additionally you\n can check inside before_save filters, if the association was altered."
|
14
|
-
s.email = "martin.koerner@objectfab.de"
|
15
|
-
s.
|
16
|
-
"
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
s.
|
45
|
-
|
46
|
-
s.
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
s.specification_version = 3
|
63
|
-
|
64
|
-
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
65
|
-
s.add_runtime_dependency(%q<activerecord>, [">= 0"])
|
66
|
-
s.add_development_dependency(%q<rspec>, [">= 0"])
|
67
|
-
else
|
68
|
-
s.add_dependency(%q<activerecord>, [">= 0"])
|
69
|
-
s.add_dependency(%q<rspec>, [">= 0"])
|
70
|
-
end
|
71
|
-
else
|
72
|
-
s.add_dependency(%q<activerecord>, [">= 0"])
|
73
|
-
s.add_dependency(%q<rspec>, [">= 0"])
|
74
|
-
end
|
75
|
-
end
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "deferred_associations"
|
8
|
+
s.version = "0.5.5"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Martin Koerner", "Tyler Rick", "Alessio Caiazza"]
|
12
|
+
s.date = "2013-10-25"
|
13
|
+
s.description = "Makes ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) or has_many\n association until you call model.save, allowing validation in the style of normal attributes. Additionally you\n can check inside before_save filters, if the association was altered."
|
14
|
+
s.email = "martin.koerner@objectfab.de"
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"CHANGELOG",
|
17
|
+
"Readme.markdown"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
"CHANGELOG",
|
21
|
+
"Rakefile",
|
22
|
+
"Readme.markdown",
|
23
|
+
"VERSION",
|
24
|
+
"deferred_associations.gemspec",
|
25
|
+
"init.rb",
|
26
|
+
"lib/array_to_association_wrapper.rb",
|
27
|
+
"lib/deferred_associations.rb",
|
28
|
+
"lib/has_and_belongs_to_many_with_deferred_save.rb",
|
29
|
+
"lib/has_many_with_deferred_save.rb",
|
30
|
+
"spec/db/database.yml",
|
31
|
+
"spec/db/schema.rb",
|
32
|
+
"spec/has_and_belongs_to_many_with_deferred_save_spec.rb",
|
33
|
+
"spec/has_many_with_deferred_save_spec.rb",
|
34
|
+
"spec/models/chair.rb",
|
35
|
+
"spec/models/door.rb",
|
36
|
+
"spec/models/person.rb",
|
37
|
+
"spec/models/room.rb",
|
38
|
+
"spec/models/table.rb",
|
39
|
+
"spec/spec_helper.rb"
|
40
|
+
]
|
41
|
+
s.homepage = "http://github.com/MartinKoerner/deferred_associations"
|
42
|
+
s.require_paths = ["lib"]
|
43
|
+
s.rubygems_version = "1.8.24"
|
44
|
+
s.summary = "Makes ActiveRecord defer/postpone habtm or has_many associations"
|
45
|
+
|
46
|
+
if s.respond_to? :specification_version then
|
47
|
+
s.specification_version = 3
|
48
|
+
|
49
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
50
|
+
s.add_runtime_dependency(%q<activerecord>, [">= 0"])
|
51
|
+
s.add_development_dependency(%q<rspec>, [">= 0"])
|
52
|
+
else
|
53
|
+
s.add_dependency(%q<activerecord>, [">= 0"])
|
54
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
55
|
+
end
|
56
|
+
else
|
57
|
+
s.add_dependency(%q<activerecord>, [">= 0"])
|
58
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
data/init.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require 'deferred_associations'
|
1
|
+
require 'deferred_associations'
|
File without changes
|
File without changes
|
@@ -1,142 +1,142 @@
|
|
1
|
-
module ActiveRecord
|
2
|
-
module Associations
|
3
|
-
module ClassMethods
|
4
|
-
|
5
|
-
# Instructions:
|
6
|
-
#
|
7
|
-
# Replace your existing call to has_and_belongs_to_many with has_and_belongs_to_many_with_deferred_save.
|
8
|
-
#
|
9
|
-
# Then add a validation method that adds an error if there is something wrong with the (unsaved) collection. This will prevent it from being saved if there are any errors.
|
10
|
-
#
|
11
|
-
# Example:
|
12
|
-
#
|
13
|
-
# def validate
|
14
|
-
# if people.size > maximum_occupancy
|
15
|
-
# errors.add :people, "There are too many people in this room"
|
16
|
-
# end
|
17
|
-
# end
|
18
|
-
def has_and_belongs_to_many_with_deferred_save(*args)
|
19
|
-
has_and_belongs_to_many *args
|
20
|
-
collection_name = args[0].to_s
|
21
|
-
collection_singular_ids = collection_name.singularize + "_ids"
|
22
|
-
|
23
|
-
add_deletion_callback
|
24
|
-
|
25
|
-
attr_accessor :"unsaved_#{collection_name}"
|
26
|
-
attr_accessor :"use_original_collection_reader_behavior_for_#{collection_name}"
|
27
|
-
|
28
|
-
define_method "#{collection_name}_with_deferred_save=" do |collection|
|
29
|
-
#puts "has_and_belongs_to_many_with_deferred_save: #{collection_name} = #{collection.collect(&:id).join(',')}"
|
30
|
-
self.send "unsaved_#{collection_name}=", collection
|
31
|
-
end
|
32
|
-
|
33
|
-
define_method "#{collection_name}_with_deferred_save" do |*args|
|
34
|
-
if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
|
35
|
-
self.send("#{collection_name}_without_deferred_save")
|
36
|
-
else
|
37
|
-
if self.send("unsaved_#{collection_name}").nil?
|
38
|
-
send("initialize_unsaved_#{collection_name}", *args)
|
39
|
-
end
|
40
|
-
self.send("unsaved_#{collection_name}")
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
alias_method_chain :"#{collection_name}=", 'deferred_save'
|
45
|
-
alias_method_chain :"#{collection_name}", 'deferred_save'
|
46
|
-
|
47
|
-
define_method "#{collection_singular_ids}_with_deferred_save" do |*args|
|
48
|
-
if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
|
49
|
-
self.send("#{collection_singular_ids}_without_deferred_save")
|
50
|
-
else
|
51
|
-
if self.send("unsaved_#{collection_name}").nil?
|
52
|
-
send("initialize_unsaved_#{collection_name}", *args)
|
53
|
-
end
|
54
|
-
self.send("unsaved_#{collection_name}").map { |e| e[:id] }
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
alias_method_chain :"#{collection_singular_ids}", 'deferred_save'
|
59
|
-
|
60
|
-
# only needed for ActiveRecord >= 3.0
|
61
|
-
if ActiveRecord::VERSION::STRING >= "3"
|
62
|
-
define_method "#{collection_singular_ids}_with_deferred_save=" do |ids|
|
63
|
-
ids = Array.wrap(ids).reject { |id| id.blank? }
|
64
|
-
reflection_wrapper = self.send("#{collection_name}_without_deferred_save")
|
65
|
-
new_values = reflection_wrapper.klass.find(ids)
|
66
|
-
self.send("#{collection_name}=", new_values)
|
67
|
-
end
|
68
|
-
alias_method_chain :"#{collection_singular_ids}=", 'deferred_save'
|
69
|
-
end
|
70
|
-
|
71
|
-
define_method "do_#{collection_name}_save!" do
|
72
|
-
# Question: Why do we need this @use_original_collection_reader_behavior stuff?
|
73
|
-
# Answer: Because AssociationCollection#replace(other_array) performs a diff between current_array and other_array and deletes/adds only
|
74
|
-
# records that have changed.
|
75
|
-
# In order to perform that diff, it needs to figure out what "current_array" is, so it calls our collection_with_deferred_save, not
|
76
|
-
# knowing that we've changed its behavior. It expects that method to return the elements of that collection that are in the *database*
|
77
|
-
# (the original behavior), so we have to provide that behavior... If we didn't provide it, it would end up trying to take the diff of
|
78
|
-
# two identical collections so nothing would ever get saved.
|
79
|
-
# But we only want the old behavior in this case -- most of the time we want the *new* behavior -- so we use
|
80
|
-
# @use_original_collection_reader_behavior as a switch.
|
81
|
-
|
82
|
-
self.send "use_original_collection_reader_behavior_for_#{collection_name}=", true
|
83
|
-
if self.send("unsaved_#{collection_name}").nil?
|
84
|
-
send("initialize_unsaved_#{collection_name}")
|
85
|
-
end
|
86
|
-
self.send "#{collection_name}_without_deferred_save=", self.send("unsaved_#{collection_name}")
|
87
|
-
# /\ This is where the actual save occurs.
|
88
|
-
self.send "use_original_collection_reader_behavior_for_#{collection_name}=", false
|
89
|
-
|
90
|
-
true
|
91
|
-
end
|
92
|
-
after_save "do_#{collection_name}_save!"
|
93
|
-
|
94
|
-
|
95
|
-
define_method "reload_with_deferred_save_for_#{collection_name}" do
|
96
|
-
# Reload from the *database*, discarding any unsaved changes.
|
97
|
-
self.send("reload_without_deferred_save_for_#{collection_name}").tap do
|
98
|
-
self.send "unsaved_#{collection_name}=", nil
|
99
|
-
# /\ If we didn't do this, then when we called reload, it would still have the same (possibly invalid) value of
|
100
|
-
# unsaved_collection that it had before the reload.
|
101
|
-
end
|
102
|
-
end
|
103
|
-
alias_method_chain :"reload", "deferred_save_for_#{collection_name}"
|
104
|
-
|
105
|
-
|
106
|
-
define_method "initialize_unsaved_#{collection_name}" do |*args|
|
107
|
-
#puts "Initialized to #{self.send("#{collection_name}_without_deferred_save").clone.inspect}"
|
108
|
-
elements = self.send("#{collection_name}_without_deferred_save", *args).clone
|
109
|
-
elements = ArrayToAssociationWrapper.new(elements)
|
110
|
-
elements.defer_association_methods_to self, collection_name
|
111
|
-
self.send "unsaved_#{collection_name}=", elements
|
112
|
-
# /\ We initialize it to collection_without_deferred_save in case they just loaded the object from the
|
113
|
-
# database, in which case we want unsaved_collection to start out with the "saved collection".
|
114
|
-
# Actually, this doesn't clone the Association but the elements array instead (since the clone method is
|
115
|
-
# proxied like any other methods)
|
116
|
-
# Important: If we don't use clone, then it does an assignment by reference and any changes to unsaved_collection
|
117
|
-
# will also change *collection_without_deferred_save*! (Not what we want! Would result in us saving things
|
118
|
-
# immediately, which is exactly what we're trying to avoid.)
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
end
|
123
|
-
private :"initialize_unsaved_#{collection_name}"
|
124
|
-
|
125
|
-
end
|
126
|
-
|
127
|
-
def add_deletion_callback
|
128
|
-
# this will delete all the association into the join table after obj.destroy,
|
129
|
-
# but is only useful/necessary, if the record is not paranoid?
|
130
|
-
unless (self.respond_to?(:paranoid?) && self.paranoid?)
|
131
|
-
after_destroy { |record|
|
132
|
-
begin
|
133
|
-
record.save
|
134
|
-
rescue Exception => e
|
135
|
-
logger.warn "Association cleanup after destroy failed with #{e}"
|
136
|
-
end
|
137
|
-
}
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
module ClassMethods
|
4
|
+
|
5
|
+
# Instructions:
|
6
|
+
#
|
7
|
+
# Replace your existing call to has_and_belongs_to_many with has_and_belongs_to_many_with_deferred_save.
|
8
|
+
#
|
9
|
+
# Then add a validation method that adds an error if there is something wrong with the (unsaved) collection. This will prevent it from being saved if there are any errors.
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# def validate
|
14
|
+
# if people.size > maximum_occupancy
|
15
|
+
# errors.add :people, "There are too many people in this room"
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
def has_and_belongs_to_many_with_deferred_save(*args)
|
19
|
+
has_and_belongs_to_many *args
|
20
|
+
collection_name = args[0].to_s
|
21
|
+
collection_singular_ids = collection_name.singularize + "_ids"
|
22
|
+
|
23
|
+
add_deletion_callback
|
24
|
+
|
25
|
+
attr_accessor :"unsaved_#{collection_name}"
|
26
|
+
attr_accessor :"use_original_collection_reader_behavior_for_#{collection_name}"
|
27
|
+
|
28
|
+
define_method "#{collection_name}_with_deferred_save=" do |collection|
|
29
|
+
#puts "has_and_belongs_to_many_with_deferred_save: #{collection_name} = #{collection.collect(&:id).join(',')}"
|
30
|
+
self.send "unsaved_#{collection_name}=", collection
|
31
|
+
end
|
32
|
+
|
33
|
+
define_method "#{collection_name}_with_deferred_save" do |*args|
|
34
|
+
if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
|
35
|
+
self.send("#{collection_name}_without_deferred_save")
|
36
|
+
else
|
37
|
+
if self.send("unsaved_#{collection_name}").nil?
|
38
|
+
send("initialize_unsaved_#{collection_name}", *args)
|
39
|
+
end
|
40
|
+
self.send("unsaved_#{collection_name}")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
alias_method_chain :"#{collection_name}=", 'deferred_save'
|
45
|
+
alias_method_chain :"#{collection_name}", 'deferred_save'
|
46
|
+
|
47
|
+
define_method "#{collection_singular_ids}_with_deferred_save" do |*args|
|
48
|
+
if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
|
49
|
+
self.send("#{collection_singular_ids}_without_deferred_save")
|
50
|
+
else
|
51
|
+
if self.send("unsaved_#{collection_name}").nil?
|
52
|
+
send("initialize_unsaved_#{collection_name}", *args)
|
53
|
+
end
|
54
|
+
self.send("unsaved_#{collection_name}").map { |e| e[:id] }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
alias_method_chain :"#{collection_singular_ids}", 'deferred_save'
|
59
|
+
|
60
|
+
# only needed for ActiveRecord >= 3.0
|
61
|
+
if ActiveRecord::VERSION::STRING >= "3"
|
62
|
+
define_method "#{collection_singular_ids}_with_deferred_save=" do |ids|
|
63
|
+
ids = Array.wrap(ids).reject { |id| id.blank? }
|
64
|
+
reflection_wrapper = self.send("#{collection_name}_without_deferred_save")
|
65
|
+
new_values = reflection_wrapper.klass.find(ids)
|
66
|
+
self.send("#{collection_name}=", new_values)
|
67
|
+
end
|
68
|
+
alias_method_chain :"#{collection_singular_ids}=", 'deferred_save'
|
69
|
+
end
|
70
|
+
|
71
|
+
define_method "do_#{collection_name}_save!" do
|
72
|
+
# Question: Why do we need this @use_original_collection_reader_behavior stuff?
|
73
|
+
# Answer: Because AssociationCollection#replace(other_array) performs a diff between current_array and other_array and deletes/adds only
|
74
|
+
# records that have changed.
|
75
|
+
# In order to perform that diff, it needs to figure out what "current_array" is, so it calls our collection_with_deferred_save, not
|
76
|
+
# knowing that we've changed its behavior. It expects that method to return the elements of that collection that are in the *database*
|
77
|
+
# (the original behavior), so we have to provide that behavior... If we didn't provide it, it would end up trying to take the diff of
|
78
|
+
# two identical collections so nothing would ever get saved.
|
79
|
+
# But we only want the old behavior in this case -- most of the time we want the *new* behavior -- so we use
|
80
|
+
# @use_original_collection_reader_behavior as a switch.
|
81
|
+
|
82
|
+
self.send "use_original_collection_reader_behavior_for_#{collection_name}=", true
|
83
|
+
if self.send("unsaved_#{collection_name}").nil?
|
84
|
+
send("initialize_unsaved_#{collection_name}")
|
85
|
+
end
|
86
|
+
self.send "#{collection_name}_without_deferred_save=", self.send("unsaved_#{collection_name}")
|
87
|
+
# /\ This is where the actual save occurs.
|
88
|
+
self.send "use_original_collection_reader_behavior_for_#{collection_name}=", false
|
89
|
+
|
90
|
+
true
|
91
|
+
end
|
92
|
+
after_save "do_#{collection_name}_save!"
|
93
|
+
|
94
|
+
|
95
|
+
define_method "reload_with_deferred_save_for_#{collection_name}" do
|
96
|
+
# Reload from the *database*, discarding any unsaved changes.
|
97
|
+
self.send("reload_without_deferred_save_for_#{collection_name}").tap do
|
98
|
+
self.send "unsaved_#{collection_name}=", nil
|
99
|
+
# /\ If we didn't do this, then when we called reload, it would still have the same (possibly invalid) value of
|
100
|
+
# unsaved_collection that it had before the reload.
|
101
|
+
end
|
102
|
+
end
|
103
|
+
alias_method_chain :"reload", "deferred_save_for_#{collection_name}"
|
104
|
+
|
105
|
+
|
106
|
+
define_method "initialize_unsaved_#{collection_name}" do |*args|
|
107
|
+
#puts "Initialized to #{self.send("#{collection_name}_without_deferred_save").clone.inspect}"
|
108
|
+
elements = self.send("#{collection_name}_without_deferred_save", *args).clone
|
109
|
+
elements = ArrayToAssociationWrapper.new(elements)
|
110
|
+
elements.defer_association_methods_to self, collection_name
|
111
|
+
self.send "unsaved_#{collection_name}=", elements
|
112
|
+
# /\ We initialize it to collection_without_deferred_save in case they just loaded the object from the
|
113
|
+
# database, in which case we want unsaved_collection to start out with the "saved collection".
|
114
|
+
# Actually, this doesn't clone the Association but the elements array instead (since the clone method is
|
115
|
+
# proxied like any other methods)
|
116
|
+
# Important: If we don't use clone, then it does an assignment by reference and any changes to unsaved_collection
|
117
|
+
# will also change *collection_without_deferred_save*! (Not what we want! Would result in us saving things
|
118
|
+
# immediately, which is exactly what we're trying to avoid.)
|
119
|
+
|
120
|
+
|
121
|
+
|
122
|
+
end
|
123
|
+
private :"initialize_unsaved_#{collection_name}"
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
def add_deletion_callback
|
128
|
+
# this will delete all the association into the join table after obj.destroy,
|
129
|
+
# but is only useful/necessary, if the record is not paranoid?
|
130
|
+
unless (self.respond_to?(:paranoid?) && self.paranoid?)
|
131
|
+
after_destroy { |record|
|
132
|
+
begin
|
133
|
+
record.save
|
134
|
+
rescue Exception => e
|
135
|
+
logger.warn "Association cleanup after destroy failed with #{e}"
|
136
|
+
end
|
137
|
+
}
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|