dm-core 0.9.11 → 0.10.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 (194) hide show
  1. data/.autotest +17 -14
  2. data/.gitignore +3 -1
  3. data/FAQ +6 -5
  4. data/History.txt +5 -50
  5. data/Manifest.txt +66 -76
  6. data/QUICKLINKS +1 -1
  7. data/README.txt +21 -15
  8. data/Rakefile +6 -7
  9. data/SPECS +2 -29
  10. data/TODO +1 -1
  11. data/deps.rip +2 -0
  12. data/dm-core.gemspec +11 -15
  13. data/lib/dm-core.rb +105 -110
  14. data/lib/dm-core/adapters.rb +135 -16
  15. data/lib/dm-core/adapters/abstract_adapter.rb +251 -181
  16. data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
  17. data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
  18. data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
  19. data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
  20. data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
  21. data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
  22. data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
  23. data/lib/dm-core/associations/many_to_many.rb +372 -90
  24. data/lib/dm-core/associations/many_to_one.rb +220 -73
  25. data/lib/dm-core/associations/one_to_many.rb +319 -255
  26. data/lib/dm-core/associations/one_to_one.rb +66 -53
  27. data/lib/dm-core/associations/relationship.rb +561 -156
  28. data/lib/dm-core/collection.rb +1101 -379
  29. data/lib/dm-core/core_ext/kernel.rb +12 -0
  30. data/lib/dm-core/core_ext/symbol.rb +10 -0
  31. data/lib/dm-core/identity_map.rb +4 -34
  32. data/lib/dm-core/migrations.rb +1283 -0
  33. data/lib/dm-core/model.rb +570 -369
  34. data/lib/dm-core/model/descendant_set.rb +81 -0
  35. data/lib/dm-core/model/hook.rb +45 -0
  36. data/lib/dm-core/model/is.rb +32 -0
  37. data/lib/dm-core/model/property.rb +247 -0
  38. data/lib/dm-core/model/relationship.rb +335 -0
  39. data/lib/dm-core/model/scope.rb +90 -0
  40. data/lib/dm-core/property.rb +808 -273
  41. data/lib/dm-core/property_set.rb +141 -98
  42. data/lib/dm-core/query.rb +1037 -483
  43. data/lib/dm-core/query/conditions/comparison.rb +872 -0
  44. data/lib/dm-core/query/conditions/operation.rb +221 -0
  45. data/lib/dm-core/query/direction.rb +43 -0
  46. data/lib/dm-core/query/operator.rb +84 -0
  47. data/lib/dm-core/query/path.rb +138 -0
  48. data/lib/dm-core/query/sort.rb +45 -0
  49. data/lib/dm-core/repository.rb +210 -94
  50. data/lib/dm-core/resource.rb +641 -421
  51. data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
  52. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
  53. data/lib/dm-core/support/chainable.rb +22 -0
  54. data/lib/dm-core/support/deprecate.rb +12 -0
  55. data/lib/dm-core/support/logger.rb +13 -0
  56. data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
  57. data/lib/dm-core/transaction.rb +333 -92
  58. data/lib/dm-core/type.rb +98 -60
  59. data/lib/dm-core/types/boolean.rb +1 -1
  60. data/lib/dm-core/types/discriminator.rb +34 -20
  61. data/lib/dm-core/types/object.rb +7 -4
  62. data/lib/dm-core/types/paranoid_boolean.rb +11 -9
  63. data/lib/dm-core/types/paranoid_datetime.rb +11 -9
  64. data/lib/dm-core/types/serial.rb +3 -3
  65. data/lib/dm-core/types/text.rb +3 -4
  66. data/lib/dm-core/version.rb +1 -1
  67. data/script/performance.rb +102 -109
  68. data/script/profile.rb +169 -38
  69. data/spec/lib/adapter_helpers.rb +105 -0
  70. data/spec/lib/collection_helpers.rb +18 -0
  71. data/spec/lib/counter_adapter.rb +34 -0
  72. data/spec/lib/pending_helpers.rb +27 -0
  73. data/spec/lib/rspec_immediate_feedback_formatter.rb +53 -0
  74. data/spec/public/associations/many_to_many_spec.rb +193 -0
  75. data/spec/public/associations/many_to_one_spec.rb +73 -0
  76. data/spec/public/associations/one_to_many_spec.rb +77 -0
  77. data/spec/public/associations/one_to_one_spec.rb +156 -0
  78. data/spec/public/collection_spec.rb +65 -0
  79. data/spec/public/migrations_spec.rb +359 -0
  80. data/spec/public/model/relationship_spec.rb +924 -0
  81. data/spec/public/model_spec.rb +159 -0
  82. data/spec/public/property_spec.rb +829 -0
  83. data/spec/public/resource_spec.rb +71 -0
  84. data/spec/public/sel_spec.rb +44 -0
  85. data/spec/public/setup_spec.rb +145 -0
  86. data/spec/public/shared/association_collection_shared_spec.rb +317 -0
  87. data/spec/public/shared/collection_shared_spec.rb +1670 -0
  88. data/spec/public/shared/finder_shared_spec.rb +1619 -0
  89. data/spec/public/shared/resource_shared_spec.rb +924 -0
  90. data/spec/public/shared/sel_shared_spec.rb +112 -0
  91. data/spec/public/transaction_spec.rb +129 -0
  92. data/spec/public/types/discriminator_spec.rb +130 -0
  93. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  94. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
  95. data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
  96. data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
  97. data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
  98. data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
  99. data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
  100. data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
  101. data/spec/semipublic/associations/relationship_spec.rb +194 -0
  102. data/spec/semipublic/associations_spec.rb +177 -0
  103. data/spec/semipublic/collection_spec.rb +142 -0
  104. data/spec/semipublic/property_spec.rb +61 -0
  105. data/spec/semipublic/query/conditions_spec.rb +528 -0
  106. data/spec/semipublic/query/path_spec.rb +443 -0
  107. data/spec/semipublic/query_spec.rb +2626 -0
  108. data/spec/semipublic/resource_spec.rb +47 -0
  109. data/spec/semipublic/shared/condition_shared_spec.rb +9 -0
  110. data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
  111. data/spec/spec.opts +3 -1
  112. data/spec/spec_helper.rb +80 -57
  113. data/tasks/ci.rb +19 -31
  114. data/tasks/dm.rb +43 -48
  115. data/tasks/doc.rb +8 -11
  116. data/tasks/gemspec.rb +5 -5
  117. data/tasks/hoe.rb +15 -16
  118. data/tasks/install.rb +8 -10
  119. metadata +74 -111
  120. data/lib/dm-core/associations.rb +0 -207
  121. data/lib/dm-core/associations/relationship_chain.rb +0 -81
  122. data/lib/dm-core/auto_migrations.rb +0 -105
  123. data/lib/dm-core/dependency_queue.rb +0 -32
  124. data/lib/dm-core/hook.rb +0 -11
  125. data/lib/dm-core/is.rb +0 -16
  126. data/lib/dm-core/logger.rb +0 -232
  127. data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
  128. data/lib/dm-core/migrator.rb +0 -29
  129. data/lib/dm-core/scope.rb +0 -58
  130. data/lib/dm-core/support.rb +0 -7
  131. data/lib/dm-core/support/array.rb +0 -13
  132. data/lib/dm-core/support/assertions.rb +0 -8
  133. data/lib/dm-core/support/errors.rb +0 -23
  134. data/lib/dm-core/support/kernel.rb +0 -11
  135. data/lib/dm-core/support/symbol.rb +0 -41
  136. data/lib/dm-core/type_map.rb +0 -80
  137. data/lib/dm-core/types.rb +0 -19
  138. data/script/all +0 -4
  139. data/spec/integration/association_spec.rb +0 -1382
  140. data/spec/integration/association_through_spec.rb +0 -203
  141. data/spec/integration/associations/many_to_many_spec.rb +0 -449
  142. data/spec/integration/associations/many_to_one_spec.rb +0 -163
  143. data/spec/integration/associations/one_to_many_spec.rb +0 -188
  144. data/spec/integration/auto_migrations_spec.rb +0 -413
  145. data/spec/integration/collection_spec.rb +0 -1073
  146. data/spec/integration/data_objects_adapter_spec.rb +0 -32
  147. data/spec/integration/dependency_queue_spec.rb +0 -46
  148. data/spec/integration/model_spec.rb +0 -197
  149. data/spec/integration/mysql_adapter_spec.rb +0 -85
  150. data/spec/integration/postgres_adapter_spec.rb +0 -731
  151. data/spec/integration/property_spec.rb +0 -253
  152. data/spec/integration/query_spec.rb +0 -514
  153. data/spec/integration/repository_spec.rb +0 -61
  154. data/spec/integration/resource_spec.rb +0 -513
  155. data/spec/integration/sqlite3_adapter_spec.rb +0 -352
  156. data/spec/integration/sti_spec.rb +0 -273
  157. data/spec/integration/strategic_eager_loading_spec.rb +0 -156
  158. data/spec/integration/transaction_spec.rb +0 -75
  159. data/spec/integration/type_spec.rb +0 -275
  160. data/spec/lib/logging_helper.rb +0 -18
  161. data/spec/lib/mock_adapter.rb +0 -27
  162. data/spec/lib/model_loader.rb +0 -100
  163. data/spec/lib/publicize_methods.rb +0 -28
  164. data/spec/models/content.rb +0 -16
  165. data/spec/models/vehicles.rb +0 -34
  166. data/spec/models/zoo.rb +0 -48
  167. data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
  168. data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
  169. data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
  170. data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
  171. data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
  172. data/spec/unit/associations/many_to_many_spec.rb +0 -32
  173. data/spec/unit/associations/many_to_one_spec.rb +0 -159
  174. data/spec/unit/associations/one_to_many_spec.rb +0 -393
  175. data/spec/unit/associations/one_to_one_spec.rb +0 -7
  176. data/spec/unit/associations/relationship_spec.rb +0 -71
  177. data/spec/unit/associations_spec.rb +0 -242
  178. data/spec/unit/auto_migrations_spec.rb +0 -111
  179. data/spec/unit/collection_spec.rb +0 -182
  180. data/spec/unit/data_mapper_spec.rb +0 -35
  181. data/spec/unit/identity_map_spec.rb +0 -126
  182. data/spec/unit/is_spec.rb +0 -80
  183. data/spec/unit/migrator_spec.rb +0 -33
  184. data/spec/unit/model_spec.rb +0 -321
  185. data/spec/unit/naming_conventions_spec.rb +0 -36
  186. data/spec/unit/property_set_spec.rb +0 -90
  187. data/spec/unit/property_spec.rb +0 -753
  188. data/spec/unit/query_spec.rb +0 -571
  189. data/spec/unit/repository_spec.rb +0 -93
  190. data/spec/unit/resource_spec.rb +0 -649
  191. data/spec/unit/scope_spec.rb +0 -142
  192. data/spec/unit/transaction_spec.rb +0 -493
  193. data/spec/unit/type_map_spec.rb +0 -114
  194. data/spec/unit/type_spec.rb +0 -119
@@ -1,107 +1,254 @@
1
1
  module DataMapper
2
2
  module Associations
3
- module ManyToOne
4
- extend Assertions
5
-
6
- # Setup many to one relationship between two models
7
- # -
8
- # @api private
9
- def self.setup(name, model, options = {})
10
- assert_kind_of 'name', name, Symbol
11
- assert_kind_of 'model', model, Model
12
- assert_kind_of 'options', options, Hash
13
-
14
- repository_name = model.repository.name
15
-
16
- model.class_eval <<-EOS, __FILE__, __LINE__
17
- def #{name}
18
- #{name}_association.nil? ? nil : #{name}_association
19
- end
3
+ module ManyToOne #:nodoc:
4
+ # Relationship class with implementation specific
5
+ # to n side of 1 to n association
6
+ class Relationship < Associations::Relationship
7
+ OPTIONS = superclass::OPTIONS.dup << :nullable
8
+
9
+ # TODO: document
10
+ # @api semipublic
11
+ alias source_repository_name child_repository_name
12
+
13
+ # TODO: document
14
+ # @api semipublic
15
+ alias source_model child_model
16
+
17
+ # TODO: document
18
+ # @api semipublic
19
+ alias target_repository_name parent_repository_name
20
+
21
+ # TODO: document
22
+ # @api semipublic
23
+ alias target_model parent_model
24
+
25
+ # TODO: document
26
+ # @api semipublic
27
+ alias target_key parent_key
28
+
29
+ # TODO: document
30
+ # @api semipublic
31
+ def nullable?
32
+ @nullable
33
+ end
20
34
 
21
- def #{name}=(parent)
22
- #{name}_association.replace(parent)
23
- end
35
+ # Returns a set of keys that identify child model
36
+ #
37
+ # @return [DataMapper::PropertySet] a set of properties that identify child model
38
+ # @api private
39
+ def child_key
40
+ return @child_key if defined?(@child_key)
41
+
42
+ repository_name = child_repository_name || parent_repository_name
43
+ properties = child_model.properties(repository_name)
24
44
 
25
- private
45
+ child_key = parent_key.zip(@child_properties || []).map do |parent_property, property_name|
46
+ property_name ||= "#{name}_#{parent_property.name}".to_sym
26
47
 
27
- def #{name}_association
28
- @#{name}_association ||= begin
29
- unless relationship = model.relationships(#{repository_name.inspect})[:#{name}]
30
- raise ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}"
48
+ properties[property_name] || begin
49
+ # create the property within the correct repository
50
+ DataMapper.repository(repository_name) do
51
+ type = parent_property.send(parent_property.type == DataMapper::Types::Boolean ? :type : :primitive)
52
+ child_model.property(property_name, type, child_key_options(parent_property))
31
53
  end
32
- association = Proxy.new(relationship, self)
33
- child_associations << association
34
- association
35
54
  end
36
55
  end
37
- EOS
38
-
39
- model.relationships(repository_name)[name] = Relationship.new(
40
- name,
41
- repository_name,
42
- model,
43
- options.fetch(:class_name, Extlib::Inflection.classify(name)),
44
- options
45
- )
46
- end
47
-
48
- class Proxy
49
- include Assertions
50
56
 
51
- instance_methods.each { |m| undef_method m unless %w[ __id__ __send__ object_id kind_of? respond_to? assert_kind_of should should_not instance_variable_set instance_variable_get ].include?(m.to_s) }
57
+ @child_key = properties.class.new(child_key).freeze
58
+ end
52
59
 
53
- def replace(parent)
54
- @parent = parent
55
- @relationship.attach_parent(@child, @parent)
56
- self
60
+ # TODO: document
61
+ # @api semipublic
62
+ alias source_key child_key
63
+
64
+ # Returns a Resoruce for this relationship with a given source
65
+ #
66
+ # @param [Resource] source
67
+ # A Resource to scope the collection with
68
+ # @param [Query] other_query (optional)
69
+ # A Query to further scope the collection with
70
+ #
71
+ # @return [Resource]
72
+ # The resource scoped to the relationship, source and query
73
+ #
74
+ # @api private
75
+ def resource_for(source, other_query = nil)
76
+ query = query_for(source, other_query)
77
+
78
+ # TODO: lookup the resource in the Identity Map, and make sure
79
+ # it matches the query criteria, otherwise perform the query
80
+
81
+ target_model.first(query)
57
82
  end
58
83
 
59
- def save
60
- return false if @parent.nil?
61
- return true unless parent.new_record?
84
+ # Loads and returns association target (ex.: author) for given source resource
85
+ # (ex.: article)
86
+ #
87
+ # @param source [DataMapper::Resource]
88
+ # Child object (ex.: instance of article)
89
+ # @param other_query [DataMapper::Query]
90
+ # Query options
91
+ #
92
+ # @api semipublic
93
+ def get(source, other_query = nil)
94
+ assert_kind_of 'source', source, source_model
95
+
96
+ lazy_load(source) unless loaded?(source)
97
+
98
+ resource = get!(source)
99
+ if other_query.nil? || query_for(source, other_query).conditions.matches?(resource)
100
+ resource
101
+ end
102
+ end
62
103
 
63
- @relationship.with_repository(parent) do
64
- result = parent.save
65
- @relationship.child_key.set(@child, @relationship.parent_key.get(parent)) if result
66
- result
104
+ # Sets value of association target (ex.: author) for given source resource
105
+ # (ex.: article)
106
+ #
107
+ # @param source [DataMapper::Resource]
108
+ # Child object (ex.: instance of article)
109
+ #
110
+ # @param source [DataMapper::Resource]
111
+ # Parent object (ex.: instance of author)
112
+ #
113
+ # @api semipublic
114
+ def set(source, target)
115
+ assert_kind_of 'source', source, source_model
116
+ assert_kind_of 'target', target, target_model, Hash, NilClass
117
+
118
+ if target.kind_of?(Hash)
119
+ target = target_model.new(target)
67
120
  end
121
+
122
+ source_key.set(source, target.nil? ? [] : target_key.get(target))
123
+ set!(source, target)
68
124
  end
69
125
 
70
- def reload
71
- @parent = nil
72
- self
126
+ # TODO: document
127
+ # @api private
128
+ def inherited_by(model)
129
+ model.relationships(source_repository_name)[name] ||
130
+ self.class.new(name, model, parent_model_name, options_with_inverse)
131
+ end
132
+
133
+ private
134
+
135
+ # Initializes the relationship, always using max cardinality of 1.
136
+ #
137
+ # @api semipublic
138
+ def initialize(name, source_model, target_model, options = {})
139
+ @nullable = options.fetch(:nullable, false)
140
+ target_model ||= Extlib::Inflection.camelize(name)
141
+ options = { :min => @nullable ? 0 : 1, :max => 1 }.update(options)
142
+ super
73
143
  end
74
144
 
75
- def kind_of?(klass)
76
- super || parent.kind_of?(klass)
145
+ # Dynamically defines reader method for source side of association
146
+ # (for instance, method article for model Paragraph)
147
+ #
148
+ # @api semipublic
149
+ def create_reader
150
+ return if source_model.resource_method_defined?(name.to_s)
151
+
152
+ source_model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
153
+ public # TODO: make this configurable
154
+
155
+ # FIXME: if the writer is used, caching nil in the ivar
156
+ # and then the FK(s) are set, the cache in the writer should
157
+ # be cleared.
158
+
159
+ def #{name}(query = nil) # def article(query = nil)
160
+ relationships[#{name.inspect}].get(self, query) # relationships["article"].get(self, query)
161
+ end # end
162
+ RUBY
77
163
  end
78
164
 
79
- def respond_to?(method, include_private = false)
80
- super || parent.respond_to?(method, include_private)
165
+ # Dynamically defines writer method for source side of association
166
+ # (for instance, method article= for model Paragraph)
167
+ #
168
+ # @api semipublic
169
+ def create_writer
170
+ writer_name = "#{name}="
171
+
172
+ return if source_model.resource_method_defined?(writer_name)
173
+
174
+ source_model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
175
+ public # TODO: make this configurable
176
+ def #{writer_name}(target) # def article=(target)
177
+ relationships[#{name.inspect}].set(self, target) # relationships["article"].set(self, target)
178
+ end # end
179
+ RUBY
81
180
  end
82
181
 
83
- def instance_variable_get(variable)
84
- super || parent.instance_variable_get(variable)
182
+ # Loads association target and sets resulting value on
183
+ # given source resource
184
+ #
185
+ # @param [Resource] source
186
+ # the source resource for the association
187
+ #
188
+ # @return [undefined]
189
+ #
190
+ # @api private
191
+ def lazy_load(source)
192
+ return unless source_key.get(source).all?
193
+
194
+ # SEL: load all related resources in the source collection
195
+ if source.saved? && source.collection.size > 1
196
+ eager_load(source.collection)
197
+ end
198
+
199
+ unless loaded?(source)
200
+ set!(source, resource_for(source))
201
+ end
85
202
  end
86
203
 
87
- private
204
+ # Sets the association targets in the resource
205
+ #
206
+ # @param [Resource] source
207
+ # the source to set
208
+ # @param [Array(Resource)] targets
209
+ # the target resource for the association
210
+ # @param [Query, Hash] query
211
+ # not used
212
+ #
213
+ # @return [undefined]
214
+ #
215
+ # @api private
216
+ def eager_load_targets(source, targets, query)
217
+ set(source, targets.first)
218
+ end
88
219
 
89
- def initialize(relationship, child)
90
- assert_kind_of 'relationship', relationship, Relationship
91
- assert_kind_of 'child', child, Resource
220
+ # Returns the inverse relationship class
221
+ #
222
+ # @api private
223
+ def inverse_class
224
+ OneToMany::Relationship
225
+ end
92
226
 
93
- @relationship = relationship
94
- @child = child
227
+ # Returns the inverse relationship name
228
+ #
229
+ # @api private
230
+ def inverse_name
231
+ super || Extlib::Inflection.underscore(Extlib::Inflection.demodulize(source_model.name)).pluralize.to_sym
95
232
  end
96
233
 
97
- def parent
98
- @parent ||= @relationship.get_parent(@child)
234
+ # TODO: document
235
+ # @api private
236
+ def child_key_options(parent_property)
237
+ options = parent_property.options.only(:length, :precision, :scale).update(:index => name, :nullable => nullable?)
238
+
239
+ if parent_property.primitive == Integer && parent_property.min && parent_property.max
240
+ options.update(:min => parent_property.min, :max => parent_property.max)
241
+ end
242
+
243
+ options
99
244
  end
100
245
 
101
- def method_missing(method, *args, &block)
102
- parent.__send__(method, *args, &block)
246
+ # TODO: document
247
+ # @api private
248
+ def child_properties
249
+ child_key.map { |property| property.name }
103
250
  end
104
- end # class Proxy
251
+ end # class Relationship
105
252
  end # module ManyToOne
106
253
  end # module Associations
107
254
  end # module DataMapper
@@ -1,315 +1,379 @@
1
1
  module DataMapper
2
2
  module Associations
3
- module OneToMany
4
- extend Assertions
5
-
6
- # Setup one to many relationship between two models
7
- # -
8
- # @api private
9
- def self.setup(name, model, options = {})
10
- assert_kind_of 'name', name, Symbol
11
- assert_kind_of 'model', model, Model
12
- assert_kind_of 'options', options, Hash
13
-
14
- repository_name = model.repository.name
15
-
16
- model.class_eval <<-EOS, __FILE__, __LINE__
17
- def #{name}(query = {})
18
- #{name}_association.all(query)
19
- end
3
+ module OneToMany #:nodoc:
4
+ class Relationship < Associations::Relationship
5
+ # TODO: document
6
+ # @api semipublic
7
+ alias target_repository_name child_repository_name
20
8
 
21
- def #{name}=(children)
22
- #{name}_association.replace(children)
23
- end
9
+ # TODO: document
10
+ # @api semipublic
11
+ alias target_model child_model
24
12
 
25
- private
26
-
27
- def #{name}_association
28
- @#{name}_association ||= begin
29
- unless relationship = model.relationships(#{repository_name.inspect})[#{name.inspect}]
30
- raise ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}"
31
- end
32
- association = Proxy.new(relationship, self)
33
- parent_associations << association
34
- association
35
- end
36
- end
37
- EOS
13
+ # TODO: document
14
+ # @api semipublic
15
+ alias source_repository_name parent_repository_name
38
16
 
39
- model.relationships(repository_name)[name] = if options.has_key?(:through)
40
- opts = options.dup
17
+ # TODO: document
18
+ # @api semipublic
19
+ alias source_model parent_model
41
20
 
42
- if opts.key?(:class_name) && !opts.key?(:child_key)
43
- warn(<<-EOS.margin)
44
- You have specified #{model.base_model.name}.has(#{name.inspect}) with :class_name => #{opts[:class_name].inspect}. You probably also want to specify the :child_key option.
45
- EOS
46
- end
21
+ # TODO: document
22
+ # @api semipublic
23
+ alias source_key parent_key
47
24
 
48
- opts[:child_model] ||= opts.delete(:class_name) || Extlib::Inflection.classify(name)
49
- opts[:parent_model] = model
50
- opts[:repository_name] = repository_name
51
- opts[:near_relationship_name] = opts.delete(:through)
52
- opts[:remote_relationship_name] ||= opts.delete(:remote_name) || name
53
- opts[:parent_key] = opts[:parent_key]
54
- opts[:child_key] = opts[:child_key]
55
-
56
- RelationshipChain.new( opts )
57
- else
58
- Relationship.new(
59
- name,
60
- repository_name,
61
- options.fetch(:class_name, Extlib::Inflection.classify(name)),
62
- model,
63
- options
64
- )
25
+ # TODO: document
26
+ # @api semipublic
27
+ def child_key
28
+ inverse.child_key
65
29
  end
66
- end
67
30
 
68
- # TODO: look at making this inherit from Collection. The API is
69
- # almost identical, and it would make more sense for the
70
- # relationship.get_children method to return a Proxy than a
71
- # Collection that is wrapped in a Proxy.
72
- class Proxy
73
- include Assertions
31
+ # TODO: document
32
+ # @api semipublic
33
+ alias target_key child_key
74
34
 
75
- instance_methods.each { |m| undef_method m unless %w[ __id__ __send__ class object_id kind_of? respond_to? assert_kind_of should should_not instance_variable_set instance_variable_get ].include?(m.to_s) }
35
+ # Returns a Collection for this relationship with a given source
36
+ #
37
+ # @param [Resource] source
38
+ # A Resource to scope the collection with
39
+ # @param [Query] other_query (optional)
40
+ # A Query to further scope the collection with
41
+ #
42
+ # @return [Collection]
43
+ # The collection scoped to the relationship, source and query
44
+ #
45
+ # @api private
46
+ def collection_for(source, other_query = nil)
47
+ query = query_for(source, other_query)
76
48
 
77
- # FIXME: remove when RelationshipChain#get_children can return a Collection
78
- def all(query = {})
79
- query.empty? ? self : @relationship.get_children(@parent, query)
80
- end
49
+ collection = collection_class.new(query)
50
+ collection.relationship = self
51
+ collection.source = source
81
52
 
82
- # FIXME: remove when RelationshipChain#get_children can return a Collection
83
- def first(*args)
84
- if args.last.respond_to?(:merge)
85
- query = args.pop
86
- @relationship.get_children(@parent, query, :first, *args)
87
- else
88
- children.first(*args)
89
- end
90
- end
53
+ # make the collection empty if the source is not saved
54
+ collection.replace([]) unless source.saved?
91
55
 
92
- def <<(resource)
93
- assert_mutable
94
- return self if !resource.new_record? && self.include?(resource)
95
- children << resource
96
- relate_resource(resource)
97
- self
56
+ collection
98
57
  end
99
58
 
100
- def push(*resources)
101
- assert_mutable
102
- resources.reject! { |resource| !resource.new_record? && self.include?(resource) }
103
- children.push(*resources)
104
- resources.each { |resource| relate_resource(resource) }
105
- self
106
- end
59
+ # Loads and returns association targets (ex.: articles) for given source resource
60
+ # (ex.: author)
61
+ #
62
+ # @api semipublic
63
+ def get(source, other_query = nil)
64
+ assert_kind_of 'source', source, source_model
107
65
 
108
- def unshift(*resources)
109
- assert_mutable
110
- resources.reject! { |resource| !resource.new_record? && self.include?(resource) }
111
- children.unshift(*resources)
112
- resources.each { |resource| relate_resource(resource) }
113
- self
114
- end
66
+ lazy_load(source) unless loaded?(source)
115
67
 
116
- def replace(other)
117
- assert_mutable
118
- each { |resource| orphan_resource(resource) }
119
- other = other.map { |resource| resource.kind_of?(Hash) ? new_child(resource) : resource }
120
- children.replace(other)
121
- other.each { |resource| relate_resource(resource) }
122
- self
68
+ collection = get!(source)
69
+ other_query.nil? ? collection : collection.all(other_query)
123
70
  end
124
71
 
125
- def pop
126
- assert_mutable
127
- orphan_resource(children.pop)
128
- end
72
+ # Sets value of association targets (ex.: paragraphs) for given source resource
73
+ # (ex.: article)
74
+ #
75
+ # @api semipublic
76
+ def set(source, targets)
77
+ assert_kind_of 'source', source, source_model
78
+ assert_kind_of 'targets', targets, Array
129
79
 
130
- def shift
131
- assert_mutable
132
- orphan_resource(children.shift)
133
- end
80
+ lazy_load(source) unless loaded?(source)
134
81
 
135
- def delete(resource)
136
- assert_mutable
137
- orphan_resource(children.delete(resource))
82
+ get!(source).replace(targets)
138
83
  end
139
84
 
140
- def delete_at(index)
141
- assert_mutable
142
- orphan_resource(children.delete_at(index))
85
+ # TODO: document
86
+ # @api private
87
+ def inherited_by(model)
88
+ model.relationships(source_repository_name)[name] ||
89
+ self.class.new(name, child_model_name, model, options_with_inverse)
143
90
  end
144
91
 
145
- def clear
146
- assert_mutable
147
- each { |resource| orphan_resource(resource) }
148
- children.clear
149
- self
150
- end
151
-
152
- def build(attributes = {})
153
- assert_mutable
154
- attributes = default_attributes.merge(attributes)
155
- resource = children.respond_to?(:build) ? children.build(attributes) : new_child(attributes)
156
- resource
157
- end
158
-
159
- def new(attributes = {})
160
- assert_mutable
161
- raise UnsavedParentError, 'You cannot intialize until the parent is saved' if @parent.new_record?
162
- attributes = default_attributes.merge(attributes)
163
- resource = children.respond_to?(:new) ? children.new(attributes) : @relationship.child_model.new(attributes)
164
- self << resource
165
- resource
166
- end
167
-
168
- def create(attributes = {})
169
- assert_mutable
170
- raise UnsavedParentError, 'You cannot create until the parent is saved' if @parent.new_record?
171
- attributes = default_attributes.merge(attributes)
172
- resource = children.respond_to?(:create) ? children.create(attributes) : @relationship.child_model.create(attributes)
173
- self << resource
174
- resource
175
- end
176
-
177
- def update(attributes = {})
178
- assert_mutable
179
- raise UnsavedParentError, 'You cannot mass-update until the parent is saved' if @parent.new_record?
180
- children.update(attributes)
181
- end
182
-
183
- def update!(attributes = {})
184
- assert_mutable
185
- raise UnsavedParentError, 'You cannot mass-update without validations until the parent is saved' if @parent.new_record?
186
- children.update!(attributes)
187
- end
188
-
189
- def destroy
190
- assert_mutable
191
- raise UnsavedParentError, 'You cannot mass-delete until the parent is saved' if @parent.new_record?
192
- children.destroy
193
- end
194
-
195
- def destroy!
196
- assert_mutable
197
- raise UnsavedParentError, 'You cannot mass-delete without validations until the parent is saved' if @parent.new_record?
198
- children.destroy!
199
- end
200
-
201
- def reload
202
- @children = nil
203
- self
204
- end
205
-
206
- def save
207
- return true if children.frozen?
208
-
209
- # save every resource in the collection
210
- each { |resource| save_resource(resource) }
92
+ private
211
93
 
212
- # save orphan resources
213
- @orphans.each do |resource|
214
- begin
215
- save_resource(resource, nil)
216
- rescue
217
- children << resource unless children.frozen? || children.include?(resource)
218
- raise
219
- end
94
+ # TODO: document
95
+ # @api semipublic
96
+ def initialize(name, target_model, source_model, options = {})
97
+ target_model ||= Extlib::Inflection.camelize(name.to_s.singular)
98
+ options = { :min => 0, :max => source_model.n }.update(options)
99
+ super
100
+ end
101
+
102
+ # Dynamically defines reader method for source side of association
103
+ # (for instance, method paragraphs for model Article)
104
+ #
105
+ # @api semipublic
106
+ def create_reader
107
+ return if source_model.resource_method_defined?(name.to_s)
108
+
109
+ source_model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
110
+ def #{name}(query = nil) # def paragraphs(query = nil)
111
+ relationships[#{name.inspect}].get(self, query) # relationships[:paragraphs].get(self, query)
112
+ end # end
113
+ RUBY
114
+ end
115
+
116
+ # Dynamically defines reader method for source side of association
117
+ # (for instance, method paragraphs= for model Article)
118
+ #
119
+ # @api semipublic
120
+ def create_writer
121
+ writer_name = "#{name}="
122
+
123
+ return if source_model.resource_method_defined?(writer_name)
124
+
125
+ source_model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
126
+ def #{writer_name}(targets) # def paragraphs=(targets)
127
+ relationships[#{name.inspect}].set(self, targets) # relationships[:paragraphs].set(self, targets)
128
+ end # end
129
+ RUBY
130
+ end
131
+
132
+ # Loads association targets and sets resulting value on
133
+ # given source resource
134
+ #
135
+ # @param [Resource] source
136
+ # the source resource for the association
137
+ #
138
+ # @return [undefined]
139
+ #
140
+ # @api private
141
+ def lazy_load(source)
142
+ # SEL: load all related resources in the source collection
143
+ if source.saved? && source.collection.size > 1
144
+ eager_load(source.collection)
220
145
  end
221
146
 
222
- # FIXME: remove when RelationshipChain#get_children can return a Collection
223
- # place the children into a Collection if not already
224
- if children.kind_of?(Array) && !children.frozen?
225
- @children = @relationship.get_children(@parent).replace(children)
147
+ unless loaded?(source)
148
+ set!(source, collection_for(source))
226
149
  end
227
-
228
- true
229
150
  end
230
151
 
231
- def kind_of?(klass)
232
- super || children.kind_of?(klass)
152
+ # Sets the association targets in the resource
153
+ #
154
+ # @param [Resource] source
155
+ # the source to set
156
+ # @param [Array<Resource>] targets
157
+ # the target collection for the association
158
+ # @param [Query, Hash] query
159
+ # the query to scope the association with
160
+ #
161
+ # @return [undefined]
162
+ #
163
+ # @api private
164
+ def eager_load_targets(source, targets, query)
165
+ # TODO: figure out an alternative approach to using a
166
+ # private method call collection_replace
167
+ association = collection_for(source, query)
168
+ association.send(:collection_replace, targets)
169
+ set!(source, association)
170
+ end
171
+
172
+ # Returns collection class used by this type of
173
+ # relationship
174
+ #
175
+ # @api private
176
+ def collection_class
177
+ OneToMany::Collection
178
+ end
179
+
180
+ # Returns the inverse relationship class
181
+ #
182
+ # @api private
183
+ def inverse_class
184
+ ManyToOne::Relationship
185
+ end
186
+
187
+ # Returns the inverse relationship name
188
+ #
189
+ # @api private
190
+ def inverse_name
191
+ super || Extlib::Inflection.underscore(Extlib::Inflection.demodulize(source_model.name)).to_sym
192
+ end
193
+
194
+ # TODO: document
195
+ # @api private
196
+ def child_properties
197
+ super || parent_key.map do |parent_property|
198
+ "#{inverse_name}_#{parent_property.name}".to_sym
199
+ end
233
200
  end
234
-
235
- def respond_to?(method, include_private = false)
236
- super || children.respond_to?(method, include_private)
201
+ end # class Relationship
202
+
203
+ class Collection < DataMapper::Collection
204
+ # TODO: document
205
+ # @api private
206
+ attr_accessor :relationship
207
+
208
+ # TODO: document
209
+ # @api private
210
+ attr_accessor :source
211
+
212
+ # TODO: document
213
+ # @api public
214
+ def reload(*)
215
+ assert_source_saved 'The source must be saved before reloading the collection'
216
+ super
217
+ end
218
+
219
+ # Replace the Resources within the 1:m Collection
220
+ #
221
+ # @param [Enumerable] other
222
+ # List of other Resources to replace with
223
+ #
224
+ # @return [Collection]
225
+ # self
226
+ #
227
+ # @api public
228
+ def replace(*)
229
+ lazy_load # lazy load so that targets are always orphaned
230
+ super
231
+ end
232
+
233
+ # Removes all Resources from the 1:m Collection
234
+ #
235
+ # This should remove and orphan each Resource from the 1:m Collection.
236
+ #
237
+ # @return [Collection]
238
+ # self
239
+ #
240
+ # @api public
241
+ def clear
242
+ lazy_load # lazy load so that targets are always orphaned
243
+ super
244
+ end
245
+
246
+ # Update every Resource in the 1:m Collection
247
+ #
248
+ # @param [Hash] attributes
249
+ # attributes to update with
250
+ #
251
+ # @return [Boolean]
252
+ # true if the resources were successfully updated
253
+ #
254
+ # @api public
255
+ def update(*)
256
+ assert_source_saved 'The source must be saved before mass-updating the collection'
257
+ super
258
+ end
259
+
260
+ # Update every Resource in the 1:m Collection, bypassing validation
261
+ #
262
+ # @param [Hash] attributes
263
+ # attributes to update
264
+ #
265
+ # @return [Boolean]
266
+ # true if the resources were successfully updated
267
+ #
268
+ # @api public
269
+ def update!(*)
270
+ assert_source_saved 'The source must be saved before mass-updating the collection'
271
+ super
272
+ end
273
+
274
+ # Remove every Resource in the 1:m Collection from the repository
275
+ #
276
+ # This performs a deletion of each Resource in the Collection from
277
+ # the repository and clears the Collection.
278
+ #
279
+ # @return [Boolean]
280
+ # true if the resources were successfully destroyed
281
+ #
282
+ # @api public
283
+ def destroy
284
+ assert_source_saved 'The source must be saved before mass-deleting the collection'
285
+ super
286
+ end
287
+
288
+ # Remove every Resource in the 1:m Collection from the repository, bypassing validation
289
+ #
290
+ # This performs a deletion of each Resource in the Collection from
291
+ # the repository and clears the Collection while skipping
292
+ # validation.
293
+ #
294
+ # @return [Boolean]
295
+ # true if the resources were successfully destroyed
296
+ #
297
+ # @api public
298
+ def destroy!
299
+ assert_source_saved 'The source must be saved before mass-deleting the collection'
300
+ super
237
301
  end
238
302
 
239
303
  private
240
304
 
241
- def initialize(relationship, parent)
242
- assert_kind_of 'relationship', relationship, Relationship
243
- assert_kind_of 'parent', parent, Resource
244
-
245
- @relationship = relationship
246
- @parent = parent
247
- @orphans = []
305
+ # TODO: document
306
+ # @api private
307
+ def _create(*)
308
+ assert_source_saved 'The source must be saved before creating a resource'
309
+ super
248
310
  end
249
311
 
250
- def children
251
- @children ||= @relationship.get_children(@parent)
252
- end
312
+ # TODO: document
313
+ # @api private
314
+ def _save(safe)
315
+ assert_source_saved 'The source must be saved before saving the collection'
253
316
 
254
- def assert_mutable
255
- raise ImmutableAssociationError, 'You can not modify this association' if children.frozen?
317
+ # update removed resources to not reference the source
318
+ @removed.all? { |resource| resource.send(safe ? :save : :save!) } && super
256
319
  end
257
320
 
258
- def default_attributes
259
- default_attributes = {}
260
-
261
- @relationship.query.each do |attribute, value|
262
- next if Query::OPTIONS.include?(attribute) || attribute.kind_of?(Query::Operator)
263
- default_attributes[attribute] = value
321
+ # TODO: document
322
+ # @api private
323
+ def lazy_load
324
+ if source.saved?
325
+ super
264
326
  end
327
+ end
265
328
 
266
- @relationship.child_key.zip(@relationship.parent_key.get(@parent)) do |property,value|
267
- default_attributes[property.name] = value
268
- end
329
+ # TODO: document
330
+ # @api private
331
+ def new_collection(query, resources = nil, &block)
332
+ collection = self.class.new(query, &block)
269
333
 
270
- default_attributes
271
- end
334
+ collection.relationship = relationship
335
+ collection.source = source
272
336
 
273
- def add_default_association_values(resource)
274
- default_attributes.each do |attribute, value|
275
- next if !resource.respond_to?("#{attribute}=") || resource.attribute_loaded?(attribute)
276
- resource.send("#{attribute}=", value)
337
+ resources ||= filter(query) if loaded?
338
+
339
+ # set the resources after the relationship and source are set
340
+ if resources
341
+ collection.send(:collection_replace, resources)
277
342
  end
278
- end
279
343
 
280
- def new_child(attributes)
281
- @relationship.child_model.new(default_attributes.merge(attributes))
344
+ collection
282
345
  end
283
346
 
284
- def relate_resource(resource)
285
- assert_mutable
286
- add_default_association_values(resource)
287
- @orphans.delete(resource)
288
- resource
347
+ # TODO: document
348
+ # @api private
349
+ def resource_added(resource)
350
+ inverse_set(resource, source)
351
+ super
289
352
  end
290
353
 
291
- def orphan_resource(resource)
292
- assert_mutable
293
- @orphans << resource
294
- resource
354
+ # TODO: document
355
+ # @api private
356
+ def resource_removed(resource)
357
+ inverse_set(resource, nil)
358
+ super
295
359
  end
296
360
 
297
- def save_resource(resource, parent = @parent)
298
- @relationship.with_repository(resource) do |r|
299
- if parent.nil? && resource.model.respond_to?(:many_to_many)
300
- resource.destroy
301
- else
302
- @relationship.attach_parent(resource, parent)
303
- resource.save
304
- end
361
+ # TODO: document
362
+ # @api private
363
+ def inverse_set(source, target)
364
+ unless source.frozen?
365
+ relationship.inverse.set(source, target)
305
366
  end
306
367
  end
307
368
 
308
- def method_missing(method, *args, &block)
309
- results = children.send(method, *args, &block)
310
- results.equal?(children) ? self : results
369
+ # TODO: document
370
+ # @api private
371
+ def assert_source_saved(message)
372
+ unless source.saved?
373
+ raise UnsavedParentError, message
374
+ end
311
375
  end
312
- end # class Proxy
376
+ end # class Collection
313
377
  end # module OneToMany
314
378
  end # module Associations
315
379
  end # module DataMapper