Alessio Caiazza is sharing code with you
Bitbucket is a code hosting site. Unlimited public and private repositories. Free for small teams.
Don't show this againhabtm-with-deferred-save / has_and_belongs_to_many_with_deferred_save / lib / has_and_belongs_to_many_with_deferred_save.rb
- commit
- c7287bf557dd
- parent
- d8a7e572f5cc
- branch
- default
FIX: delete all the assocation into the join table after obj.destroy
1 |
2498251996b3
|
# To do: make it work to call this twice in a class. Currently that probably wouldn't work, because it would try to alias methods to existing names... |
2 |
2498251996b3
|
# Note: before_save must be defined *before* including this module, not after. |
3 |
2498251996b3
|
|
4 |
2498251996b3
|
module ActiveRecord |
5 |
2498251996b3
|
module Associations |
6 |
2498251996b3
|
module ClassMethods |
7 |
2498251996b3
|
# Instructions: |
8 |
2498251996b3
|
# |
9 |
2498251996b3
|
# Replace your existing call to has_and_belongs_to_many with has_and_belongs_to_many_with_deferred_save. |
10 |
2498251996b3
|
# |
11 |
2498251996b3
|
# 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. |
12 |
2498251996b3
|
# |
13 |
2498251996b3
|
# Example: |
14 |
2498251996b3
|
# |
15 |
2498251996b3
|
# def validate |
16 |
2498251996b3
|
# if people.size > maximum_occupancy |
17 |
2498251996b3
|
# errors.add :people, "There are too many people in this room" |
18 |
2498251996b3
|
# end |
19 |
2498251996b3
|
# end |
20 |
2498251996b3
|
def has_and_belongs_to_many_with_deferred_save(*args) |
21 |
2498251996b3
|
has_and_belongs_to_many *args |
22 |
2498251996b3
|
collection_name = args[0].to_s |
23 |
356177366517
|
collection_singular_ids = collection_name.singularize + "_ids" |
24 |
c7287bf557dd
|
|
25 |
c7287bf557dd
|
# this will delete all the assocation into the join table after obj.destroy |
26 |
c7287bf557dd
|
after_destroy { |record| record.save } |
27 |
2498251996b3
|
|
28 |
2498251996b3
|
attr_accessor :"unsaved_#{collection_name}" |
29 |
3ed2e6884cb2
|
attr_accessor :"use_original_collection_reader_behavior_for_#{collection_name}" |
30 |
2498251996b3
|
|
31 |
2498251996b3
|
define_method "#{collection_name}_with_deferred_save=" do |collection| |
32 |
2498251996b3
|
#puts "has_and_belongs_to_many_with_deferred_save: #{collection_name} = #{collection.collect(&:id).join(',')}" |
33 |
2498251996b3
|
self.send "unsaved_#{collection_name}=", collection |
34 |
2498251996b3
|
end |
35 |
2498251996b3
|
|
36 |
2498251996b3
|
define_method "#{collection_name}_with_deferred_save" do |*args| |
37 |
3ed2e6884cb2
|
if self.send("use_original_collection_reader_behavior_for_#{collection_name}") |
38 |
2498251996b3
|
self.send("#{collection_name}_without_deferred_save") |
39 |
2498251996b3
|
else |
40 |
2498251996b3
|
if self.send("unsaved_#{collection_name}").nil? |
41 |
2498251996b3
|
send("initialize_unsaved_#{collection_name}", *args) |
42 |
2498251996b3
|
end |
43 |
2498251996b3
|
self.send("unsaved_#{collection_name}") |
44 |
2498251996b3
|
end |
45 |
2498251996b3
|
end |
46 |
2498251996b3
|
|
47 |
2498251996b3
|
alias_method_chain :"#{collection_name}=", 'deferred_save' |
48 |
2498251996b3
|
alias_method_chain :"#{collection_name}", 'deferred_save' |
49 |
2498251996b3
|
|
50 |
356177366517
|
define_method "#{collection_singular_ids}_with_deferred_save" do |*args| |
51 |
356177366517
|
if self.send("use_original_collection_reader_behavior_for_#{collection_name}") |
52 |
356177366517
|
self.send("#{collection_singular_ids}_without_deferred_save") |
53 |
356177366517
|
else |
54 |
356177366517
|
if self.send("unsaved_#{collection_name}").nil? |
55 |
356177366517
|
send("initialize_unsaved_#{collection_name}", *args) |
56 |
356177366517
|
end |
57 |
356177366517
|
self.send("unsaved_#{collection_name}").map { |e| e[:id] } |
58 |
356177366517
|
end |
59 |
356177366517
|
end |
60 |
356177366517
|
|
61 |
356177366517
|
alias_method_chain :"#{collection_singular_ids}", 'deferred_save' |
62 |
356177366517
|
|
63 |
2498251996b3
|
|
64 |
3ed2e6884cb2
|
define_method "before_save_with_deferred_save_for_#{collection_name}" do |
65 |
2498251996b3
|
# Question: Why do we need this @use_original_collection_reader_behavior stuff? |
66 |
2498251996b3
|
# Answer: Because AssociationCollection#replace(other_array) performs a diff between current_array and other_array and deletes/adds only |
67 |
2498251996b3
|
# records that have changed. |
68 |
2498251996b3
|
# In order to perform that diff, it needs to figure out what "current_array" is, so it calls our collection_with_deferred_save, not |
69 |
2498251996b3
|
# knowing that we've changed its behavior. It expects that method to return the elements of that collection that are in the *database* |
70 |
2498251996b3
|
# (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 |
71 |
2498251996b3
|
# two identical collections so nothing would ever get saved. |
72 |
2498251996b3
|
# But we only want the old behavior in this case -- most of the time we want the *new* behavior -- so we use |
73 |
2498251996b3
|
# @use_original_collection_reader_behavior as a switch. |
74 |
3ed2e6884cb2
|
|
75 |
3ed2e6884cb2
|
if self.respond_to? :"before_save_without_deferred_save_for_#{collection_name}" |
76 |
3ed2e6884cb2
|
self.send("before_save_without_deferred_save_for_#{collection_name}") |
77 |
3ed2e6884cb2
|
end |
78 |
2498251996b3
|
|
79 |
3ed2e6884cb2
|
self.send "use_original_collection_reader_behavior_for_#{collection_name}=", true |
80 |
d8a7e572f5cc
|
if self.send("unsaved_#{collection_name}").nil? |
81 |
d8a7e572f5cc
|
send("initialize_unsaved_#{collection_name}", *args) |
82 |
d8a7e572f5cc
|
end |
83 |
2498251996b3
|
self.send "#{collection_name}_without_deferred_save=", self.send("unsaved_#{collection_name}") |
84 |
2498251996b3
|
# /\ This is where the actual save occurs. |
85 |
3ed2e6884cb2
|
self.send "use_original_collection_reader_behavior_for_#{collection_name}=", false |
86 |
2498251996b3
|
|
87 |
2498251996b3
|
true |
88 |
2498251996b3
|
end |
89 |
3ed2e6884cb2
|
alias_method_chain :"before_save", "deferred_save_for_#{collection_name}" |
90 |
2498251996b3
|
|
91 |
2498251996b3
|
|
92 |
3ed2e6884cb2
|
define_method "reload_with_deferred_save_for_#{collection_name}" do |
93 |
2498251996b3
|
# Reload from the *database*, discarding any unsaved changes. |
94 |
3ed2e6884cb2
|
returning self.send("reload_without_deferred_save_for_#{collection_name}") do |
95 |
2498251996b3
|
self.send "unsaved_#{collection_name}=", nil |
96 |
2498251996b3
|
# /\ If we didn't do this, then when we called reload, it would still have the same (possibly invalid) value of |
97 |
2498251996b3
|
# unsaved_collection that it had before the reload. |
98 |
2498251996b3
|
end |
99 |
2498251996b3
|
end |
100 |
3ed2e6884cb2
|
alias_method_chain :"reload", "deferred_save_for_#{collection_name}" |
101 |
2498251996b3
|
|
102 |
2498251996b3
|
|
103 |
2498251996b3
|
define_method "initialize_unsaved_#{collection_name}" do |*args| |
104 |
2498251996b3
|
#puts "Initialized to #{self.send("#{collection_name}_without_deferred_save").clone.inspect}" |
105 |
2498251996b3
|
self.send "unsaved_#{collection_name}=", self.send("#{collection_name}_without_deferred_save", *args).clone |
106 |
2498251996b3
|
# /\ We initialize it to collection_without_deferred_save in case they just loaded the object from the |
107 |
2498251996b3
|
# database, in which case we want unsaved_collection to start out with the "saved collection". |
108 |
2498251996b3
|
# If they just constructed a *new* object, this will still work, because self.collection_without_deferred_save.clone |
109 |
2498251996b3
|
# will return a new HasAndBelongsToManyAssociation (which acts like an empty array, []). |
110 |
2498251996b3
|
# Important: If we don't use clone, then it does an assignment by reference and any changes to unsaved_collection |
111 |
2498251996b3
|
# will also change *collection_without_deferred_save*! (Not what we want! Would result in us saving things |
112 |
2498251996b3
|
# immediately, which is exactly what we're trying to avoid.) |
113 |
356177366517
|
|
114 |
356177366517
|
# trick collection_name.include?(obj) |
115 |
356177366517
|
# If you use a collection of SignelTableInheritance and didn't :select 'type' the |
116 |
356177366517
|
# include? method will not find any subclassed object. |
117 |
356177366517
|
class << eval("@unsaved_#{collection_name}") |
118 |
356177366517
|
def include_with_deferred_save?(obj) |
119 |
356177366517
|
if self.find { |itm| itm == obj || (itm[:id] == obj[:id] && obj.is_a?(itm.class) ) } |
120 |
356177366517
|
return true |
121 |
356177366517
|
else |
122 |
356177366517
|
return false |
123 |
356177366517
|
end |
124 |
356177366517
|
end |
125 |
356177366517
|
alias_method_chain :include?, 'deferred_save' |
126 |
356177366517
|
end |
127 |
2498251996b3
|
end |
128 |
356177366517
|
private :"initialize_unsaved_#{collection_name}" |
129 |
356177366517
|
|
130 |
2498251996b3
|
end |
131 |
2498251996b3
|
end |
132 |
2498251996b3
|
end |
133 |
2498251996b3
|
end |