tenacity 0.3.0 → 0.4.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.
Files changed (70) hide show
  1. data/EXTEND.rdoc +9 -13
  2. data/Rakefile +6 -0
  3. data/history.txt +17 -0
  4. data/lib/tenacity.rb +13 -9
  5. data/lib/tenacity/associate_proxy.rb +56 -0
  6. data/lib/tenacity/associates_proxy.rb +5 -3
  7. data/lib/tenacity/association.rb +81 -9
  8. data/lib/tenacity/associations/belongs_to.rb +28 -16
  9. data/lib/tenacity/associations/has_many.rb +113 -53
  10. data/lib/tenacity/associations/has_one.rb +33 -14
  11. data/lib/tenacity/class_methods.rb +117 -9
  12. data/lib/tenacity/errors.rb +9 -0
  13. data/lib/tenacity/instance_methods.rb +40 -3
  14. data/lib/tenacity/orm_ext/activerecord.rb +114 -95
  15. data/lib/tenacity/orm_ext/couchrest.rb +132 -113
  16. data/lib/tenacity/orm_ext/datamapper.rb +138 -112
  17. data/lib/tenacity/orm_ext/helpers.rb +36 -0
  18. data/lib/tenacity/orm_ext/mongo_mapper.rb +102 -84
  19. data/lib/tenacity/orm_ext/mongoid.rb +106 -87
  20. data/lib/tenacity/orm_ext/sequel.rb +137 -110
  21. data/lib/tenacity/version.rb +1 -1
  22. data/tenacity.gemspec +2 -1
  23. data/test/association_features/belongs_to_test.rb +46 -1
  24. data/test/association_features/has_many_test.rb +187 -2
  25. data/test/association_features/has_one_test.rb +57 -2
  26. data/test/associations/belongs_to_test.rb +41 -7
  27. data/test/associations/has_many_test.rb +59 -10
  28. data/test/associations/has_one_test.rb +57 -2
  29. data/test/fixtures/active_record_car.rb +5 -4
  30. data/test/fixtures/active_record_climate_control_unit.rb +1 -0
  31. data/test/fixtures/active_record_engine.rb +1 -0
  32. data/test/fixtures/active_record_has_many_target.rb +7 -0
  33. data/test/fixtures/active_record_has_one_target.rb +7 -0
  34. data/test/fixtures/active_record_object.rb +19 -0
  35. data/test/fixtures/couch_rest_has_many_target.rb +7 -0
  36. data/test/fixtures/couch_rest_has_one_target.rb +7 -0
  37. data/test/fixtures/couch_rest_object.rb +14 -0
  38. data/test/fixtures/data_mapper_has_many_target.rb +23 -3
  39. data/test/fixtures/data_mapper_has_one_target.rb +23 -3
  40. data/test/fixtures/data_mapper_object.rb +14 -0
  41. data/test/fixtures/mongo_mapper_air_filter.rb +6 -0
  42. data/test/fixtures/mongo_mapper_alternator.rb +7 -0
  43. data/test/fixtures/mongo_mapper_autosave_false_has_many_target.rb +8 -0
  44. data/test/fixtures/mongo_mapper_autosave_false_has_one_target.rb +8 -0
  45. data/test/fixtures/mongo_mapper_autosave_true_has_many_target.rb +8 -0
  46. data/test/fixtures/mongo_mapper_autosave_true_has_one_target.rb +8 -0
  47. data/test/fixtures/mongo_mapper_circuit_board.rb +6 -0
  48. data/test/fixtures/mongo_mapper_dashboard.rb +5 -2
  49. data/test/fixtures/mongo_mapper_has_many_target.rb +7 -0
  50. data/test/fixtures/mongo_mapper_has_one_target.rb +7 -0
  51. data/test/fixtures/mongo_mapper_object.rb +14 -0
  52. data/test/fixtures/mongo_mapper_wheel.rb +2 -0
  53. data/test/fixtures/mongo_mapper_windows.rb +6 -0
  54. data/test/fixtures/mongoid_has_many_target.rb +7 -0
  55. data/test/fixtures/mongoid_has_one_target.rb +7 -0
  56. data/test/fixtures/mongoid_object.rb +14 -0
  57. data/test/fixtures/sequel_has_many_target.rb +7 -0
  58. data/test/fixtures/sequel_has_one_target.rb +7 -0
  59. data/test/fixtures/sequel_object.rb +14 -0
  60. data/test/helpers/active_record_test_helper.rb +87 -8
  61. data/test/helpers/couch_rest_test_helper.rb +7 -0
  62. data/test/helpers/data_mapper_test_helper.rb +41 -3
  63. data/test/helpers/sequel_test_helper.rb +65 -9
  64. data/test/orm_ext/activerecord_test.rb +1 -1
  65. data/test/orm_ext/datamapper_test.rb +33 -24
  66. data/test/orm_ext/mongo_mapper_test.rb +6 -6
  67. data/test/orm_ext/mongoid_test.rb +6 -6
  68. data/test/orm_ext/sequel_test.rb +1 -1
  69. data/test/test_helper.rb +18 -9
  70. metadata +119 -55
data/EXTEND.rdoc CHANGED
@@ -5,14 +5,6 @@ only, so as long as they have been implemented and are available on the model
5
5
  object, Tenacity will be able to manage the object's relationships.
6
6
 
7
7
 
8
- == A note about IDs
9
-
10
- An ID can be an integer, a string, or an object, depending on the database
11
- and database client you are using. To be as compatible as possible, Tenacity
12
- treats all IDs as strings. So, all database client extensions should accept
13
- strings for IDs as input parameters, and return strings for IDs.
14
-
15
-
16
8
  == The Association Class
17
9
 
18
10
  A few of the methods take an association as a parameter. This association
@@ -56,21 +48,25 @@ delete callback meethods are not run. Return nothing.
56
48
 
57
49
  Perform any database client specific initialization necessary to support a has
58
50
  many association. This could include defining properties, or callback methods,
59
- on the object. This method is optional, and does not need to be defined.
51
+ on the object. This method must also call
52
+ _t_cleanup_has_many_association(association) to cleanup the association
53
+ when the after the object has been deleted.
60
54
 
61
55
  _t_initialize_belongs_to_association(association)
62
56
 
63
57
  Perform any database client specific initialization necessary to support a
64
58
  belongs to association. This could include defining properties, or callback
65
- methods, on the object. This method is optional, and does not need to be
66
- defined.
59
+ methods, on the object. This method must also call
60
+ _t_cleanup_belongs_to_association(association) to cleanup the association
61
+ when the after the object has been deleted.
67
62
 
68
63
  _t_initialize_has_one_association(association)
69
64
 
70
65
  Perform any database client specific initialization necessary to support a has one
71
66
  association. This could include defining properties, or callback methods,
72
- on the object. This method is optional, and does not need to be defined.
73
-
67
+ on the object. This method must also call
68
+ _t_cleanup_has_one_association(association) to cleanup the association
69
+ when the after the object has been deleted.
74
70
 
75
71
  == Instance Methods
76
72
 
data/Rakefile CHANGED
@@ -25,6 +25,12 @@ Rake::TestTask.new(:test) do |test|
25
25
  test.verbose = true
26
26
  end
27
27
 
28
+ desc 'Run an abbreviated version of the test suite'
29
+ task :quick_test do
30
+ ENV['QUICK'] = 'true'
31
+ Rake::Task["test"].invoke
32
+ end
33
+
28
34
  Rcov::RcovTask.new do |test|
29
35
  test.libs << 'test' << 'test/fixtures'
30
36
  test.pattern = 'test/**/*_test.rb'
data/history.txt CHANGED
@@ -1,3 +1,20 @@
1
+ == 0.4.0
2
+
3
+ * Major enhancements
4
+
5
+ * Tenacity will no longer convert foreign keys to strings before storing them
6
+ in the database. Instead, the ID will be stored without any modification
7
+ (when possible).
8
+
9
+ * Minor enhancements
10
+
11
+ * Added support for the :dependent option to all associations
12
+ * Added support for the :readonly option to all associations
13
+ * Added support for the :limit option to the t_has_many association
14
+ * Added support for the :offset option to the t_has_many association
15
+ * Added support for the :autosave option to all associations
16
+ * Added support for polymorphic associations to all associations
17
+
1
18
  == 0.3.0
2
19
 
3
20
  * Major enhancements
data/lib/tenacity.rb CHANGED
@@ -1,12 +1,15 @@
1
1
  require File.join('active_support', 'inflector')
2
2
 
3
+ require File.join(File.dirname(__FILE__), 'tenacity', 'associate_proxy')
3
4
  require File.join(File.dirname(__FILE__), 'tenacity', 'associates_proxy')
4
5
  require File.join(File.dirname(__FILE__), 'tenacity', 'association')
5
6
  require File.join(File.dirname(__FILE__), 'tenacity', 'class_methods')
7
+ require File.join(File.dirname(__FILE__), 'tenacity', 'errors')
6
8
  require File.join(File.dirname(__FILE__), 'tenacity', 'instance_methods')
7
9
  require File.join(File.dirname(__FILE__), 'tenacity', 'associations', 'belongs_to')
8
10
  require File.join(File.dirname(__FILE__), 'tenacity', 'associations', 'has_many')
9
11
  require File.join(File.dirname(__FILE__), 'tenacity', 'associations', 'has_one')
12
+ require File.join(File.dirname(__FILE__), 'tenacity', 'orm_ext', 'helpers')
10
13
  require File.join(File.dirname(__FILE__), 'tenacity', 'orm_ext', 'activerecord')
11
14
  require File.join(File.dirname(__FILE__), 'tenacity', 'orm_ext', 'couchrest')
12
15
  require File.join(File.dirname(__FILE__), 'tenacity', 'orm_ext', 'datamapper')
@@ -17,20 +20,21 @@ require File.join(File.dirname(__FILE__), 'tenacity', 'orm_ext', 'sequel')
17
20
  module Tenacity #:nodoc:
18
21
  include InstanceMethods
19
22
 
20
- include BelongsTo
21
- include HasMany
22
- include HasOne
23
+ include Associations::BelongsTo
24
+ include Associations::HasMany
25
+ include Associations::HasOne
23
26
 
24
27
  def self.included(model)
25
- ActiveRecord.setup(model)
26
- CouchRest.setup(model)
27
- DataMapper.setup(model)
28
- MongoMapper.setup(model)
29
- Mongoid.setup(model)
30
- Sequel.setup(model)
28
+ OrmExt::ActiveRecord.setup(model)
29
+ OrmExt::CouchRest.setup(model)
30
+ OrmExt::DataMapper.setup(model)
31
+ OrmExt::MongoMapper.setup(model)
32
+ OrmExt::Mongoid.setup(model)
33
+ OrmExt::Sequel.setup(model)
31
34
 
32
35
  raise "Tenacity does not support the database client used by #{model}" unless model.respond_to?(:_t_find)
33
36
  model.extend(ClassMethods)
37
+ model._t_initialize_tenacity
34
38
  end
35
39
  end
36
40
 
@@ -0,0 +1,56 @@
1
+ module Tenacity
2
+ class AssociateProxy #:nodoc:
3
+ alias_method :proxy_respond_to?, :respond_to?
4
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
5
+
6
+ def initialize(target, association)
7
+ raise "Cannot create a Tenacity::AssociateProxy with a nil target" if target.nil?
8
+ @target = target
9
+ @association = association
10
+ @marked_for_destruction = false
11
+ end
12
+
13
+ def respond_to?(*args)
14
+ proxy_respond_to?(*args) || @target.respond_to?(*args)
15
+ end
16
+
17
+ # Explicitly proxy === because the instance method removal above doesn't catch it.
18
+ def ===(other)
19
+ other === @target
20
+ end
21
+
22
+ def inspect
23
+ @target.inspect
24
+ end
25
+
26
+ def save
27
+ if @association.readonly?
28
+ raise ReadOnlyError
29
+ else
30
+ @target.save
31
+ end
32
+ end
33
+
34
+ def association_target
35
+ @target
36
+ end
37
+
38
+ def mark_for_destruction
39
+ @marked_for_destruction = true
40
+ end
41
+
42
+ def marked_for_destruction?
43
+ @marked_for_destruction
44
+ end
45
+
46
+ private
47
+
48
+ def method_missing(method, *args)
49
+ if block_given?
50
+ @target.send(method, *args) { |*block_args| yield(*block_args) }
51
+ else
52
+ @target.send(method, *args)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -20,17 +20,19 @@ module Tenacity
20
20
 
21
21
  def <<(object)
22
22
  object.save unless @parent.id.nil?
23
- @target << object
23
+ @target << AssociateProxy.new(object, @association)
24
24
  end
25
25
 
26
26
  def push(*objects)
27
27
  objects.each { |object| object.save } unless @parent.id.nil?
28
- @target.push(*objects)
28
+ proxies = objects.map { |object| AssociateProxy.new(object, @association) }
29
+ @target.push(*proxies)
29
30
  end
30
31
 
31
32
  def concat(objects)
32
33
  objects.each { |object| object.save } unless @parent.id.nil?
33
- @target.concat(objects)
34
+ proxies = objects.map { |object| AssociateProxy.new(object, @association) }
35
+ @target.concat(proxies)
34
36
  end
35
37
 
36
38
  def destroy_all
@@ -8,15 +8,33 @@ module Tenacity
8
8
  # Type type of the association (<tt>:t_has_one</tt>, <tt>:t_has_many</tt>, or <tt>:t_belongs_to</tt>)
9
9
  attr_reader :type
10
10
 
11
- # The name of the association
12
- attr_reader :name
13
-
14
11
  # The class defining the association
15
12
  attr_reader :source
16
13
 
17
14
  # The name of the associated class
18
15
  attr_reader :class_name
19
16
 
17
+ # What happens to the associated object when the object is deleted
18
+ attr_reader :dependent
19
+
20
+ # Are the associated objects read only?
21
+ attr_reader :readonly
22
+
23
+ # The limit on the number of results to be returned.
24
+ attr_reader :limit
25
+
26
+ # The offset from where the results should be fetched.
27
+ attr_reader :offset
28
+
29
+ # Should the associated object be saved when the parent object is saved?
30
+ attr_reader :autosave
31
+
32
+ # The interface this association is reffered to as
33
+ attr_reader :as
34
+
35
+ # Is this association a polymorphic association?
36
+ attr_reader :polymorphic
37
+
20
38
  def initialize(type, name, source, options={})
21
39
  @type = type
22
40
  @name = name
@@ -33,6 +51,13 @@ module Tenacity
33
51
  @join_table = options[:join_table]
34
52
  @association_key = options[:association_key]
35
53
  @association_foreign_key = options[:association_foreign_key]
54
+ @dependent = options[:dependent]
55
+ @readonly = options[:readonly]
56
+ @limit = options[:limit]
57
+ @offset = options[:offset]
58
+ @autosave = options[:autosave]
59
+ @polymorphic = options[:polymorphic]
60
+ @as = options[:as]
36
61
 
37
62
  if @foreign_keys_property
38
63
  if @foreign_keys_property.to_s == ActiveSupport::Inflector.singularize(name) + "_ids"
@@ -41,9 +66,18 @@ module Tenacity
41
66
  end
42
67
  end
43
68
 
69
+ # The name of the association
70
+ def name
71
+ @as.nil? ? @name : @as
72
+ end
73
+
44
74
  # Get the associated class
45
- def associate_class
46
- @clazz ||= Kernel.const_get(@class_name)
75
+ def associate_class(object=nil)
76
+ if @type == :t_belongs_to && polymorphic?
77
+ Kernel.const_get(object.send(polymorphic_type))
78
+ else
79
+ @clazz ||= Kernel.const_get(@class_name)
80
+ end
47
81
  end
48
82
 
49
83
  # Get the foreign key used by this association. <tt>t_has_one</tt> and
@@ -52,10 +86,9 @@ module Tenacity
52
86
  def foreign_key(clazz=nil)
53
87
  @foreign_key || begin
54
88
  if @type == :t_belongs_to
55
- @class_name.underscore + "_id"
89
+ belongs_to_foreign_key
56
90
  elsif @type == :t_has_one || @type == :t_has_many
57
- raise "The class of the associate must be provided in order to determine the name of the foreign key" if clazz.nil?
58
- "#{ActiveSupport::Inflector.underscore(clazz)}_id"
91
+ has_x_foreign_key(clazz)
59
92
  end
60
93
  end
61
94
  end
@@ -68,7 +101,14 @@ module Tenacity
68
101
  # Get the name of the join table used by this association
69
102
  def join_table
70
103
  table_name = fetch_table_name
71
- @join_table || (name.to_s < table_name ? "#{name}_#{table_name}" : "#{table_name}_#{name}")
104
+
105
+ if @type == :t_has_many && polymorphic?
106
+ association_name_for_join_table = name.to_s.pluralize
107
+ else
108
+ association_name_for_join_table = name
109
+ end
110
+
111
+ @join_table || (name.to_s < table_name ? "#{association_name_for_join_table}_#{table_name}" : "#{table_name}_#{association_name_for_join_table}")
72
112
  end
73
113
 
74
114
  # Get the name of the column in the join table that represents this object
@@ -82,6 +122,21 @@ module Tenacity
82
122
  @association_foreign_key || name.to_s.singularize + '_id'
83
123
  end
84
124
 
125
+ # Are the associated objects read only?
126
+ def readonly?
127
+ @readonly == true
128
+ end
129
+
130
+ # Is this association a polymorphic association?
131
+ def polymorphic?
132
+ @polymorphic == true || !@as.nil?
133
+ end
134
+
135
+ # The name of the property that stores the polymorphic type (for polymorphic associations)
136
+ def polymorphic_type
137
+ (name.to_s + "_type").to_sym
138
+ end
139
+
85
140
  private
86
141
 
87
142
  def fetch_table_name
@@ -91,5 +146,22 @@ module Tenacity
91
146
  "#{ActiveSupport::Inflector.underscore(@source)}s"
92
147
  end
93
148
  end
149
+
150
+ def belongs_to_foreign_key
151
+ if polymorphic?
152
+ (name.to_s + "_id").to_sym
153
+ else
154
+ @class_name.underscore + "_id"
155
+ end
156
+ end
157
+
158
+ def has_x_foreign_key(clazz)
159
+ raise "The class of the associate must be provided in order to determine the name of the foreign key" if clazz.nil?
160
+ if polymorphic?
161
+ (@as.to_s + "_id").to_sym
162
+ else
163
+ "#{ActiveSupport::Inflector.underscore(clazz)}_id"
164
+ end
165
+ end
94
166
  end
95
167
  end
@@ -1,28 +1,40 @@
1
1
  module Tenacity
2
- module BelongsTo #:nodoc:
2
+ module Associations
3
+ module BelongsTo #:nodoc:
3
4
 
4
- private
5
+ def _t_cleanup_belongs_to_association(association)
6
+ associate_id = self.send(association.foreign_key)
7
+ if associate_id != nil && associate_id.to_s.strip != ''
8
+ if association.dependent == :destroy
9
+ association.associate_class._t_delete(associate_id)
10
+ elsif association.dependent == :delete
11
+ association.associate_class._t_delete(associate_id, false)
12
+ end
13
+ end
14
+ end
5
15
 
6
- def belongs_to_associate(association)
7
- associate_id = self.send(association.foreign_key)
8
- clazz = association.associate_class
9
- clazz._t_find(associate_id)
10
- end
16
+ private
11
17
 
12
- def set_belongs_to_associate(association, associate)
13
- self.send "#{association.foreign_key}=", associate.id.to_s
14
- end
18
+ def belongs_to_associate(association)
19
+ associate_id = self.send(association.foreign_key)
20
+ clazz = association.associate_class(self)
21
+ associate = clazz._t_find(associate_id)
22
+ associate
23
+ end
15
24
 
16
- module ClassMethods #:nodoc:
17
- def initialize_belongs_to_association(association)
18
- _t_initialize_belongs_to_association(association) if self.respond_to?(:_t_initialize_belongs_to_association)
25
+ def set_belongs_to_associate(association, associate)
26
+ self.send "#{association.foreign_key}=", _t_serialize(associate.id)
27
+ self.send "#{association.polymorphic_type}=", associate.class.to_s if association.polymorphic?
28
+ associate
19
29
  end
20
30
 
21
- def _t_stringify_belongs_to_value(record, association)
22
- record.send "#{association.foreign_key}=", record.send(association.foreign_key).to_s
31
+ module ClassMethods #:nodoc:
32
+ def initialize_belongs_to_association(association)
33
+ _t_initialize_belongs_to_association(association) if self.respond_to?(:_t_initialize_belongs_to_association)
34
+ end
23
35
  end
24
- end
25
36
 
37
+ end
26
38
  end
27
39
  end
28
40
 
@@ -1,80 +1,140 @@
1
1
  module Tenacity
2
- module HasMany #:nodoc:
2
+ module Associations
3
+ module HasMany #:nodoc:
3
4
 
4
- def _t_remove_associates(association)
5
- instance_variable_set _t_ivar_name(association), []
6
- _t_clear_associates(association)
7
- end
5
+ def _t_remove_associates(association)
6
+ instance_variable_set _t_ivar_name(association), []
7
+ _t_clear_associates(association)
8
+ end
8
9
 
9
- private
10
+ def _t_cleanup_has_many_association(association)
11
+ associates = has_many_associates(association)
12
+ unless associates.nil? || associates.empty?
13
+ if association.dependent == :destroy
14
+ associates.each { |associate| association.associate_class._t_delete([_t_serialize(associate.id)]) }
15
+ elsif association.dependent == :delete_all
16
+ associates.each { |associate| association.associate_class._t_delete([_t_serialize(associate.id)], false) }
17
+ elsif association.dependent == :nullify
18
+ associates.each do |associate|
19
+ associate.send "#{association.foreign_key(self.class)}=", nil
20
+ associate.save
21
+ end
22
+ end
23
+ end
24
+ end
10
25
 
11
- def has_many_associates(association)
12
- ids = _t_get_associate_ids(association)
13
- clazz = association.associate_class
14
- clazz._t_find_bulk(ids)
15
- end
26
+ private
16
27
 
17
- def has_many_associate_ids(association)
18
- _t_get_associate_ids(association)
19
- end
28
+ def has_many_associates(association)
29
+ ids = _t_get_associate_ids(association)
30
+ pruned_ids = prune_associate_ids(association, ids)
31
+ clazz = association.associate_class
32
+ clazz._t_find_bulk(pruned_ids)
33
+ end
20
34
 
21
- def set_has_many_associate_ids(association, associate_ids)
22
- clazz = association.associate_class
23
- instance_variable_set _t_ivar_name(association), clazz._t_find_bulk(associate_ids)
24
- end
35
+ def set_has_many_associates(association, associates)
36
+ associates.map { |associate| AssociateProxy.new(associate, association) }
37
+ end
25
38
 
26
- def save_without_callback
27
- @perform_save_associates_callback = false
28
- save
29
- ensure
30
- @perform_save_associates_callback = true
31
- end
39
+ def has_many_associate_ids(association)
40
+ ids = _t_get_associate_ids(association)
41
+ prune_associate_ids(association, ids)
42
+ end
43
+
44
+ def set_has_many_associate_ids(association, associate_ids)
45
+ clazz = association.associate_class
46
+ instance_variable_set _t_ivar_name(association), clazz._t_find_bulk(associate_ids)
47
+ end
48
+
49
+ def save_without_callback
50
+ @perform_save_associates_callback = false
51
+ save
52
+ ensure
53
+ @perform_save_associates_callback = true
54
+ end
32
55
 
33
- module ClassMethods #:nodoc:
34
- def initialize_has_many_association(association)
35
- _t_initialize_has_many_association(association) if self.respond_to?(:_t_initialize_has_many_association)
56
+ def prune_associate_ids(association, associate_ids)
57
+ if association.limit || association.offset
58
+ sorted_ids = associate_ids.sort { |a,b| a <=> b }
36
59
 
37
- attr_accessor :perform_save_associates_callback
60
+ limit = association.limit || associate_ids.size
61
+ offset = association.offset || 0
62
+ sorted_ids[offset...(offset + limit)]
63
+ else
64
+ associate_ids
65
+ end
38
66
  end
39
67
 
40
- def _t_save_associates(record, association)
41
- return if record.perform_save_associates_callback == false
68
+ module ClassMethods #:nodoc:
69
+ def initialize_has_many_association(association)
70
+ _t_initialize_has_many_association(association) if self.respond_to?(:_t_initialize_has_many_association)
71
+
72
+ attr_accessor :perform_save_associates_callback
73
+ end
74
+
75
+ def _t_save_associates(record, association)
76
+ return if record.perform_save_associates_callback == false
77
+
78
+ old_associates = get_current_associates(record, association)
79
+
80
+ # Some ORM libraries (CouchRest, ActiveRecord, etc) return a proxy in
81
+ # place of the associated objects. The actual associated objects
82
+ # will be fetched the first time they are needed. So, force them to
83
+ # be fetched here, before we clear them out in the database.
84
+ old_associates.inspect
85
+
86
+ _t_clear_old_associations(record, association)
42
87
 
43
- _t_clear_old_associations(record, association)
88
+ associates = (record.instance_variable_get record._t_ivar_name(association)) || []
89
+ associates.each do |associate|
90
+ associate._t_reload
91
+ associate.send("#{association.foreign_key(record.class)}=", _t_serialize(record.id, association))
92
+ associate.send "#{association.polymorphic_type}=", self.to_s if association.polymorphic?
93
+ save_associate(associate)
94
+ end
44
95
 
45
- associates = (record.instance_variable_get record._t_ivar_name(association)) || []
46
- associates.each do |associate|
47
- associate._t_reload
48
- associate.send("#{association.foreign_key(record.class)}=", record.id.to_s)
49
- save_associate(associate)
96
+ unless associates.blank?
97
+ associate_ids = associates.map { |associate| _t_serialize(associate.id) }
98
+ record._t_associate_many(association, associate_ids)
99
+ save_associate(record)
100
+ end
101
+
102
+ destroy_orphaned_associates(association, old_associates, associates)
50
103
  end
51
104
 
52
- unless associates.blank?
53
- associate_ids = associates.map { |associate| associate.id.to_s }
54
- record._t_associate_many(association, associate_ids)
105
+ def _t_clear_old_associations(record, association)
106
+ property_name = association.foreign_key(record.class)
107
+ old_associates = get_current_associates(record, association)
108
+ old_associates.each do |old_associate|
109
+ old_associate.send("#{property_name}=", nil)
110
+ save_associate(old_associate)
111
+ end
112
+
113
+ record._t_clear_associates(association)
55
114
  save_associate(record)
56
115
  end
57
- end
58
116
 
59
- def _t_clear_old_associations(record, association)
60
- clazz = association.associate_class
61
- property_name = association.foreign_key(record.class)
117
+ def save_associate(associate)
118
+ associate.respond_to?(:_t_save_without_callback) ? associate._t_save_without_callback : associate.save
119
+ end
62
120
 
63
- old_associates = clazz._t_find_all_by_associate(property_name, record.id.to_s)
64
- old_associates.each do |old_associate|
65
- old_associate.send("#{property_name}=", nil)
66
- save_associate(old_associate)
121
+ def get_current_associates(record, association)
122
+ clazz = association.associate_class
123
+ property_name = association.foreign_key(record.class)
124
+ clazz._t_find_all_by_associate(property_name, _t_serialize(record.id, association))
67
125
  end
68
126
 
69
- record._t_clear_associates(association)
70
- save_associate(record)
127
+ def destroy_orphaned_associates(association, old_associates, associates)
128
+ if association.dependent == :destroy || association.dependent == :delete_all
129
+ issue_callbacks = (association.dependent == :destroy)
130
+ (old_associates.map{|a| a.id} - associates.map{|a| a.id}).each do |associate_id|
131
+ association.associate_class._t_delete([_t_serialize(associate_id)], issue_callbacks)
132
+ end
133
+ end
134
+ end
71
135
  end
72
136
 
73
- def save_associate(associate)
74
- associate.respond_to?(:_t_save_without_callback) ? associate._t_save_without_callback : associate.save
75
- end
76
137
  end
77
-
78
138
  end
79
139
  end
80
140