datamapper-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 (192) hide show
  1. data/.autotest +17 -14
  2. data/.gitignore +3 -1
  3. data/FAQ +6 -5
  4. data/History.txt +5 -39
  5. data/Manifest.txt +67 -76
  6. data/QUICKLINKS +1 -1
  7. data/README.txt +21 -15
  8. data/Rakefile +16 -15
  9. data/SPECS +2 -29
  10. data/TODO +1 -1
  11. data/dm-core.gemspec +11 -15
  12. data/lib/dm-core/adapters/abstract_adapter.rb +182 -185
  13. data/lib/dm-core/adapters/data_objects_adapter.rb +482 -534
  14. data/lib/dm-core/adapters/in_memory_adapter.rb +90 -69
  15. data/lib/dm-core/adapters/mysql_adapter.rb +22 -115
  16. data/lib/dm-core/adapters/oracle_adapter.rb +249 -0
  17. data/lib/dm-core/adapters/postgres_adapter.rb +7 -173
  18. data/lib/dm-core/adapters/sqlite3_adapter.rb +4 -97
  19. data/lib/dm-core/adapters/yaml_adapter.rb +116 -0
  20. data/lib/dm-core/adapters.rb +135 -16
  21. data/lib/dm-core/associations/many_to_many.rb +372 -90
  22. data/lib/dm-core/associations/many_to_one.rb +220 -73
  23. data/lib/dm-core/associations/one_to_many.rb +319 -255
  24. data/lib/dm-core/associations/one_to_one.rb +66 -53
  25. data/lib/dm-core/associations/relationship.rb +560 -158
  26. data/lib/dm-core/collection.rb +1104 -381
  27. data/lib/dm-core/core_ext/kernel.rb +12 -0
  28. data/lib/dm-core/core_ext/symbol.rb +10 -0
  29. data/lib/dm-core/identity_map.rb +4 -34
  30. data/lib/dm-core/migrations.rb +1283 -0
  31. data/lib/dm-core/model/descendant_set.rb +81 -0
  32. data/lib/dm-core/model/hook.rb +45 -0
  33. data/lib/dm-core/model/is.rb +32 -0
  34. data/lib/dm-core/model/property.rb +248 -0
  35. data/lib/dm-core/model/relationship.rb +335 -0
  36. data/lib/dm-core/model/scope.rb +90 -0
  37. data/lib/dm-core/model.rb +570 -369
  38. data/lib/dm-core/property.rb +753 -280
  39. data/lib/dm-core/property_set.rb +141 -98
  40. data/lib/dm-core/query/conditions/comparison.rb +814 -0
  41. data/lib/dm-core/query/conditions/operation.rb +247 -0
  42. data/lib/dm-core/query/direction.rb +43 -0
  43. data/lib/dm-core/query/operator.rb +42 -0
  44. data/lib/dm-core/query/path.rb +102 -0
  45. data/lib/dm-core/query/sort.rb +45 -0
  46. data/lib/dm-core/query.rb +974 -492
  47. data/lib/dm-core/repository.rb +147 -107
  48. data/lib/dm-core/resource.rb +644 -429
  49. data/lib/dm-core/spec/adapter_shared_spec.rb +294 -0
  50. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +106 -0
  51. data/lib/dm-core/support/chainable.rb +20 -0
  52. data/lib/dm-core/support/deprecate.rb +12 -0
  53. data/lib/dm-core/support/equalizer.rb +23 -0
  54. data/lib/dm-core/support/logger.rb +13 -0
  55. data/lib/dm-core/{naming_conventions.rb → support/naming_conventions.rb} +6 -6
  56. data/lib/dm-core/transaction.rb +333 -92
  57. data/lib/dm-core/type.rb +98 -60
  58. data/lib/dm-core/types/boolean.rb +1 -1
  59. data/lib/dm-core/types/discriminator.rb +34 -20
  60. data/lib/dm-core/types/object.rb +7 -4
  61. data/lib/dm-core/types/paranoid_boolean.rb +11 -9
  62. data/lib/dm-core/types/paranoid_datetime.rb +11 -9
  63. data/lib/dm-core/types/serial.rb +3 -3
  64. data/lib/dm-core/types/text.rb +3 -4
  65. data/lib/dm-core/version.rb +1 -1
  66. data/lib/dm-core.rb +106 -110
  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/model/relationship_spec.rb +924 -0
  80. data/spec/public/model_spec.rb +159 -0
  81. data/spec/public/property_spec.rb +829 -0
  82. data/spec/public/resource_spec.rb +71 -0
  83. data/spec/public/sel_spec.rb +44 -0
  84. data/spec/public/setup_spec.rb +145 -0
  85. data/spec/public/shared/association_collection_shared_spec.rb +317 -0
  86. data/spec/public/shared/collection_shared_spec.rb +1723 -0
  87. data/spec/public/shared/finder_shared_spec.rb +1619 -0
  88. data/spec/public/shared/resource_shared_spec.rb +924 -0
  89. data/spec/public/shared/sel_shared_spec.rb +112 -0
  90. data/spec/public/transaction_spec.rb +129 -0
  91. data/spec/public/types/discriminator_spec.rb +130 -0
  92. data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
  93. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +12 -0
  94. data/spec/semipublic/adapters/mysql_adapter_spec.rb +17 -0
  95. data/spec/semipublic/adapters/oracle_adapter_spec.rb +194 -0
  96. data/spec/semipublic/adapters/postgres_adapter_spec.rb +17 -0
  97. data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +17 -0
  98. data/spec/semipublic/adapters/yaml_adapter_spec.rb +12 -0
  99. data/spec/semipublic/associations/many_to_one_spec.rb +53 -0
  100. data/spec/semipublic/associations/relationship_spec.rb +194 -0
  101. data/spec/semipublic/associations_spec.rb +177 -0
  102. data/spec/semipublic/collection_spec.rb +142 -0
  103. data/spec/semipublic/property_spec.rb +61 -0
  104. data/spec/semipublic/query/conditions_spec.rb +528 -0
  105. data/spec/semipublic/query/path_spec.rb +443 -0
  106. data/spec/semipublic/query_spec.rb +2626 -0
  107. data/spec/semipublic/resource_spec.rb +47 -0
  108. data/spec/semipublic/shared/resource_shared_spec.rb +126 -0
  109. data/spec/spec.opts +3 -1
  110. data/spec/spec_helper.rb +80 -57
  111. data/tasks/ci.rb +19 -31
  112. data/tasks/dm.rb +43 -48
  113. data/tasks/doc.rb +8 -11
  114. data/tasks/gemspec.rb +5 -5
  115. data/tasks/hoe.rb +15 -16
  116. data/tasks/install.rb +8 -10
  117. metadata +72 -93
  118. data/lib/dm-core/associations/relationship_chain.rb +0 -81
  119. data/lib/dm-core/associations.rb +0 -207
  120. data/lib/dm-core/auto_migrations.rb +0 -105
  121. data/lib/dm-core/dependency_queue.rb +0 -32
  122. data/lib/dm-core/hook.rb +0 -11
  123. data/lib/dm-core/is.rb +0 -16
  124. data/lib/dm-core/logger.rb +0 -232
  125. data/lib/dm-core/migrations/destructive_migrations.rb +0 -17
  126. data/lib/dm-core/migrator.rb +0 -29
  127. data/lib/dm-core/scope.rb +0 -58
  128. data/lib/dm-core/support/array.rb +0 -13
  129. data/lib/dm-core/support/assertions.rb +0 -8
  130. data/lib/dm-core/support/errors.rb +0 -23
  131. data/lib/dm-core/support/kernel.rb +0 -11
  132. data/lib/dm-core/support/symbol.rb +0 -41
  133. data/lib/dm-core/support.rb +0 -7
  134. data/lib/dm-core/type_map.rb +0 -80
  135. data/lib/dm-core/types.rb +0 -19
  136. data/script/all +0 -4
  137. data/spec/integration/association_spec.rb +0 -1382
  138. data/spec/integration/association_through_spec.rb +0 -203
  139. data/spec/integration/associations/many_to_many_spec.rb +0 -449
  140. data/spec/integration/associations/many_to_one_spec.rb +0 -163
  141. data/spec/integration/associations/one_to_many_spec.rb +0 -188
  142. data/spec/integration/auto_migrations_spec.rb +0 -413
  143. data/spec/integration/collection_spec.rb +0 -1073
  144. data/spec/integration/data_objects_adapter_spec.rb +0 -32
  145. data/spec/integration/dependency_queue_spec.rb +0 -46
  146. data/spec/integration/model_spec.rb +0 -197
  147. data/spec/integration/mysql_adapter_spec.rb +0 -85
  148. data/spec/integration/postgres_adapter_spec.rb +0 -731
  149. data/spec/integration/property_spec.rb +0 -253
  150. data/spec/integration/query_spec.rb +0 -514
  151. data/spec/integration/repository_spec.rb +0 -61
  152. data/spec/integration/resource_spec.rb +0 -513
  153. data/spec/integration/sqlite3_adapter_spec.rb +0 -352
  154. data/spec/integration/sti_spec.rb +0 -273
  155. data/spec/integration/strategic_eager_loading_spec.rb +0 -156
  156. data/spec/integration/transaction_spec.rb +0 -75
  157. data/spec/integration/type_spec.rb +0 -275
  158. data/spec/lib/logging_helper.rb +0 -18
  159. data/spec/lib/mock_adapter.rb +0 -27
  160. data/spec/lib/model_loader.rb +0 -100
  161. data/spec/lib/publicize_methods.rb +0 -28
  162. data/spec/models/content.rb +0 -16
  163. data/spec/models/vehicles.rb +0 -34
  164. data/spec/models/zoo.rb +0 -48
  165. data/spec/unit/adapters/abstract_adapter_spec.rb +0 -133
  166. data/spec/unit/adapters/adapter_shared_spec.rb +0 -15
  167. data/spec/unit/adapters/data_objects_adapter_spec.rb +0 -632
  168. data/spec/unit/adapters/in_memory_adapter_spec.rb +0 -98
  169. data/spec/unit/adapters/postgres_adapter_spec.rb +0 -133
  170. data/spec/unit/associations/many_to_many_spec.rb +0 -32
  171. data/spec/unit/associations/many_to_one_spec.rb +0 -159
  172. data/spec/unit/associations/one_to_many_spec.rb +0 -393
  173. data/spec/unit/associations/one_to_one_spec.rb +0 -7
  174. data/spec/unit/associations/relationship_spec.rb +0 -71
  175. data/spec/unit/associations_spec.rb +0 -242
  176. data/spec/unit/auto_migrations_spec.rb +0 -111
  177. data/spec/unit/collection_spec.rb +0 -182
  178. data/spec/unit/data_mapper_spec.rb +0 -35
  179. data/spec/unit/identity_map_spec.rb +0 -126
  180. data/spec/unit/is_spec.rb +0 -80
  181. data/spec/unit/migrator_spec.rb +0 -33
  182. data/spec/unit/model_spec.rb +0 -321
  183. data/spec/unit/naming_conventions_spec.rb +0 -36
  184. data/spec/unit/property_set_spec.rb +0 -90
  185. data/spec/unit/property_spec.rb +0 -753
  186. data/spec/unit/query_spec.rb +0 -571
  187. data/spec/unit/repository_spec.rb +0 -93
  188. data/spec/unit/resource_spec.rb +0 -649
  189. data/spec/unit/scope_spec.rb +0 -142
  190. data/spec/unit/transaction_spec.rb +0 -493
  191. data/spec/unit/type_map_spec.rb +0 -114
  192. data/spec/unit/type_spec.rb +0 -119
@@ -1,94 +1,161 @@
1
- require 'set'
2
-
3
1
  module DataMapper
4
2
  module Resource
5
- include Assertions
3
+ include Extlib::Assertions
4
+ extend Chainable
5
+ extend Deprecate
6
6
 
7
- ##
8
- #
9
- # Appends a module for inclusion into the model class after
10
- # DataMapper::Resource.
11
- #
12
- # This is a useful way to extend DataMapper::Resource while still retaining
13
- # a self.included method.
14
- #
15
- # @param [Module] inclusion the module that is to be appended to the module
16
- # after DataMapper::Resource
17
- #
18
- # @return [TrueClass, FalseClass] whether or not the inclusions have been
19
- # successfully appended to the list
20
- # @return <TrueClass, FalseClass>
21
- #-
22
- # @api public
7
+ deprecate :new_record?, :new?
8
+
9
+ # @deprecated
23
10
  def self.append_inclusions(*inclusions)
24
- extra_inclusions.concat inclusions
25
- true
11
+ warn "DataMapper::Resource.append_inclusions is deprecated, use DataMapper::Model.append_inclusions instead (#{caller[0]})"
12
+ Model.append_inclusions(*inclusions)
26
13
  end
27
14
 
15
+ # @deprecated
28
16
  def self.extra_inclusions
29
- @extra_inclusions ||= []
17
+ warn "DataMapper::Resource.extra_inclusions is deprecated, use DataMapper::Model.extra_inclusions instead (#{caller[0]})"
18
+ Model.extra_inclusions
19
+ end
20
+
21
+ # @deprecated
22
+ def self.descendants
23
+ warn "DataMapper::Resource.descendants is deprecated, use DataMapper::Model.descendants instead (#{caller[0]})"
24
+ Model.descendants
25
+ end
26
+
27
+ # Deprecated API for updating attributes and saving Resource
28
+ #
29
+ # @see #update
30
+ #
31
+ # @deprecated
32
+ def update_attributes(attributes = {}, *allowed)
33
+ assert_update_clean_only(:update_attributes)
34
+
35
+ warn "#{model}#update_attributes is deprecated, use #{model}#update instead (#{caller[0]})"
36
+
37
+ if allowed.any?
38
+ warn "specifying allowed in #{model}#update_attributes is deprecated, " \
39
+ "use Hash#only to filter the attributes in the caller (#{caller[0]})"
40
+ attributes = attributes.only(*allowed)
41
+ end
42
+
43
+ update(attributes)
30
44
  end
31
45
 
32
- # When Resource is included in a class this method makes sure
33
- # it gets all the methods
46
+ # Makes sure a class gets all the methods when it includes Resource
34
47
  #
35
- # -
36
48
  # @api private
37
49
  def self.included(model)
38
50
  model.extend Model
39
- model.extend ClassMethods if defined?(ClassMethods)
40
- model.const_set('Resource', self) unless model.const_defined?('Resource')
41
- extra_inclusions.each { |inclusion| model.send(:include, inclusion) }
42
- descendants << model
43
- class << model
44
- @_valid_model = false
45
- attr_reader :_valid_model
46
- end
47
51
  end
48
52
 
49
- # Return all classes that include the DataMapper::Resource module
53
+ # Collection this resource associated with.
54
+ # Used by SEL.
50
55
  #
51
- # ==== Returns
52
- # Set:: a set containing the including classes
56
+ # @api private
57
+ attr_writer :collection
58
+
59
+ # TODO: document
60
+ # @api public
61
+ alias_method :model, :class
62
+
63
+ # Repository this resource belongs to in the context of this collection
64
+ # or of the resource's class.
53
65
  #
54
- # ==== Example
66
+ # @return [Repository]
67
+ # the respository this resource belongs to, in the context of
68
+ # a collection OR in the instance's Model's context
55
69
  #
56
- # Class Foo
57
- # include DataMapper::Resource
58
- # end
70
+ # @api semipublic
71
+ def repository
72
+ # only set @repository explicitly when persisted
73
+ defined?(@repository) ? @repository : model.repository
74
+ end
75
+
76
+ # Retrieve the key(s) for this resource.
59
77
  #
60
- # DataMapper::Resource.descendants.to_a.first == Foo
78
+ # This always returns the persisted key value,
79
+ # even if the key is changed and not yet persisted.
80
+ # This is done so all relations still work.
61
81
  #
62
- # -
63
- # @api semipublic
64
- def self.descendants
65
- @descendants ||= Set.new
82
+ # @return [Array(Key)]
83
+ # the key(s) identifying this resource
84
+ #
85
+ # @api public
86
+ def key
87
+ return @key if defined?(@key)
88
+
89
+ key = model.key(repository_name).map do |property|
90
+ original_attributes[property] || (property.loaded?(self) ? property.get!(self) : nil)
91
+ end
92
+
93
+ return unless key.all?
94
+
95
+ # memoize the key if the Resource is not frozen
96
+ @key = key unless frozen?
97
+
98
+ key
66
99
  end
67
100
 
68
- # +---------------
69
- # Instance methods
101
+ # Checks if this Resource instance is new
102
+ #
103
+ # @return [Boolean]
104
+ # true if the resource is new and not saved
105
+ #
106
+ # @api public
107
+ def new?
108
+ !saved?
109
+ end
70
110
 
71
- attr_writer :collection
111
+ # Checks if this Resource instance is saved
112
+ #
113
+ # @return [Boolean]
114
+ # true if the resource has been saved
115
+ #
116
+ # @api public
117
+ def saved?
118
+ @saved == true
119
+ end
72
120
 
73
- alias model class
121
+ # Checks if the resource has no changes to save
122
+ #
123
+ # @return [Boolean]
124
+ # true if the resource may not be persisted
125
+ #
126
+ # @api public
127
+ def clean?
128
+ !dirty?
129
+ end
74
130
 
75
- # returns the value of the attribute. Do not read from instance variables directly,
76
- # but use this method. This method handels the lazy loading the attribute and returning
77
- # of defaults if nessesary.
131
+ # Checks if the resource has unsaved changes
78
132
  #
79
- # ==== Parameters
80
- # name<Symbol>:: name attribute to lookup
133
+ # @return [Boolean]
134
+ # true if resource may be persisted
81
135
  #
82
- # ==== Returns
83
- # <Types>:: the value stored at that given attribute, nil if none, and default if necessary
136
+ # @api public
137
+ def dirty?
138
+ if original_attributes.any?
139
+ true
140
+ elsif new?
141
+ model.serial || properties.any? { |property| property.default? }
142
+ else
143
+ false
144
+ end
145
+ end
146
+
147
+ # Returns the value of the attribute.
84
148
  #
85
- # ==== Example
149
+ # Do not read from instance variables directly, but use this method.
150
+ # This method handles lazy loading the attribute and returning of
151
+ # defaults if nessesary.
86
152
  #
87
- # Class Foo
153
+ # @example
154
+ # class Foo
88
155
  # include DataMapper::Resource
89
156
  #
90
157
  # property :first_name, String
91
- # property :last_name, String
158
+ # property :last_name, String
92
159
  #
93
160
  # def full_name
94
161
  # "#{attribute_get(:first_name)} #{attribute_get(:last_name)}"
@@ -100,32 +167,32 @@ module DataMapper
100
167
  # end
101
168
  # end
102
169
  #
103
- # -
104
- # @api semipublic
170
+ # @param [Symbol] name
171
+ # name of attribute to retrieve
172
+ #
173
+ # @return [Object]
174
+ # the value stored at that given attribute
175
+ # (nil if none, and default if necessary)
176
+ #
177
+ # @api public
105
178
  def attribute_get(name)
106
179
  properties[name].get(self)
107
180
  end
108
181
 
109
- # sets the value of the attribute and marks the attribute as dirty
182
+ alias [] attribute_get
183
+
184
+ # Sets the value of the attribute and marks the attribute as dirty
110
185
  # if it has been changed so that it may be saved. Do not set from
111
186
  # instance variables directly, but use this method. This method
112
- # handels the lazy loading the property and returning of defaults
187
+ # handles the lazy loading the property and returning of defaults
113
188
  # if nessesary.
114
189
  #
115
- # ==== Parameters
116
- # name<Symbol>:: name attribute to set
117
- # value<Type>:: value to store at that location
118
- #
119
- # ==== Returns
120
- # <Types>:: the value stored at that given attribute, nil if none, and default if necessary
121
- #
122
- # ==== Example
123
- #
124
- # Class Foo
190
+ # @example
191
+ # class Foo
125
192
  # include DataMapper::Resource
126
193
  #
127
194
  # property :first_name, String
128
- # property :last_name, String
195
+ # property :last_name, String
129
196
  #
130
197
  # def full_name(name)
131
198
  # name = name.split(' ')
@@ -141,531 +208,679 @@ module DataMapper
141
208
  # end
142
209
  # end
143
210
  #
144
- # -
145
- # @api semipublic
211
+ # @param [Symbol] name
212
+ # name of attribute to set
213
+ # @param [Object] value
214
+ # value to store
215
+ #
216
+ # @return [Object]
217
+ # the value stored at that given attribute, nil if none,
218
+ # and default if necessary
219
+ #
220
+ # @api public
146
221
  def attribute_set(name, value)
147
222
  properties[name].set(self, value)
148
223
  end
149
224
 
150
- # Compares if its the same object or if attributes are equal
225
+ alias []= attribute_set
226
+
227
+ # Gets all the attributes of the Resource instance
151
228
  #
152
- # The comparaison is
153
- # * false if object not from same repository
154
- # * false if object has no all same properties
229
+ # @param [Symbol] key_on
230
+ # Use this attribute of the Property as keys.
231
+ # defaults to :name. :field is useful for adapters
232
+ # :property or nil use the actual Property object.
155
233
  #
234
+ # @return [Hash]
235
+ # All the attributes
156
236
  #
157
- # ==== Parameters
158
- # other<Object>:: Object to compare to
237
+ # @api public
238
+ def attributes(key_on = :name)
239
+ attributes = {}
240
+ properties.each do |property|
241
+ if model.public_method_defined?(name = property.name)
242
+ key = case key_on
243
+ when :name then name
244
+ when :field then property.field
245
+ else property
246
+ end
247
+
248
+ attributes[key] = send(name)
249
+ end
250
+ end
251
+ attributes
252
+ end
253
+
254
+ # Assign values to multiple attributes in one call (mass assignment)
255
+ #
256
+ # @param [Hash] attributes
257
+ # names and values of attributes to assign
159
258
  #
160
- # ==== Returns
161
- # <True>:: the outcome of the comparison as a boolean
259
+ # @return [Hash]
260
+ # names and values of attributes assigned
162
261
  #
163
- # -
164
262
  # @api public
165
- def eql?(other)
166
- return true if equal?(other)
167
-
168
- # two instances for different models cannot be equivalent
169
- return false unless other.kind_of?(model)
170
-
171
- # two instances with different keys cannot be equivalent
172
- return false if key != other.key
173
-
174
- # neither object has changed since loaded, so they are equivalent
175
- return true if repository == other.repository && !dirty? && !other.dirty?
176
-
177
- # get all the loaded and non-loaded properties that are not keys,
178
- # since the key comparison was performed earlier
179
- loaded, not_loaded = properties.select { |p| !p.key? }.partition do |property|
180
- attribute_loaded?(property.name) && other.attribute_loaded?(property.name)
263
+ def attributes=(attributes)
264
+ attributes.each do |name, value|
265
+ case name
266
+ when String, Symbol
267
+ if model.public_method_defined?(setter = "#{name}=")
268
+ send(setter, value)
269
+ else
270
+ raise ArgumentError, "The attribute '#{name}' is not accessible in #{model}"
271
+ end
272
+ when Associations::Relationship, Property
273
+ name.set(self, value)
274
+ end
181
275
  end
182
-
183
- # check all loaded properties, and then all unloaded properties
184
- (loaded + not_loaded).all? { |p| p.get(self) == p.get(other) }
185
276
  end
186
277
 
187
- alias == eql?
188
-
189
- # Computes a hash for the resource
278
+ # Reloads association and all child association
190
279
  #
191
- # ==== Returns
192
- # <Integer>:: the hash value of the resource
280
+ # @return [Resource]
281
+ # the receiver, the current Resource instance
193
282
  #
194
- # -
195
283
  # @api public
196
- def hash
197
- model.hash + key.hash
284
+ def reload
285
+ if saved?
286
+ eager_load(loaded_properties)
287
+ child_relationships.each { |relationship| relationship.get!(self).reload }
288
+ end
289
+
290
+ self
198
291
  end
199
292
 
200
- # Inspection of the class name and the attributes
201
- #
202
- # ==== Returns
203
- # <String>:: with the class name, attributes with their values
293
+ # Updates attributes and saves this Resource instance
204
294
  #
205
- # ==== Example
295
+ # @param [Hash] attributes
296
+ # attributes to be updated
206
297
  #
207
- # >> Foo.new
208
- # => #<Foo name=nil updated_at=nil created_at=nil id=nil>
298
+ # @return [Boolean]
299
+ # true if resource and storage state match
209
300
  #
210
- # -
211
301
  # @api public
212
- def inspect
213
- attrs = []
214
-
215
- properties.each do |property|
216
- value = if !attribute_loaded?(property.name) && !new_record?
217
- '<not loaded>'
218
- else
219
- send(property.getter).inspect
220
- end
221
-
222
- attrs << "#{property.name}=#{value}"
302
+ chainable do
303
+ def update(attributes = {})
304
+ assert_update_clean_only(:update)
305
+ self.attributes = attributes
306
+ save
223
307
  end
308
+ end
224
309
 
225
- "#<#{model.name} #{attrs * ' '}>"
310
+ # Updates attributes and saves this Resource instance, bypassing hooks
311
+ #
312
+ # @param [Hash] attributes
313
+ # attributes to be updated
314
+ #
315
+ # @return [Boolean]
316
+ # true if resource and storage state match
317
+ #
318
+ # @api public
319
+ def update!(attributes = {})
320
+ assert_update_clean_only(:update!)
321
+ self.attributes = attributes
322
+ save!
226
323
  end
227
324
 
228
- # TODO docs
229
- def pretty_print(pp)
230
- pp.group(1, "#<#{model.name}", ">") do
231
- pp.breakable
232
- pp.seplist(attributes.to_a) do |k_v|
233
- pp.text k_v[0].to_s
234
- pp.text " = "
235
- pp.pp k_v[1]
236
- end
325
+ # Save the instance and loaded, dirty associations to the data-store
326
+ #
327
+ # @return [Boolean]
328
+ # true if Resource instance and all associations were saved
329
+ #
330
+ # @api public
331
+ chainable do
332
+ def save
333
+ save_parents && save_self && save_children
237
334
  end
238
335
  end
239
336
 
240
- ##
337
+ # Save the instance and loaded, dirty associations to the data-store, bypassing hooks
241
338
  #
242
- # ==== Returns
243
- # <Repository>:: the respository this resource belongs to in the context of a collection OR in the class's context
339
+ # @return [Boolean]
340
+ # true if Resource instance and all associations were saved
244
341
  #
245
342
  # @api public
246
- def repository
247
- @repository || model.repository
343
+ def save!
344
+ save_parents(false) && save_self(false) && save_children(false)
248
345
  end
249
346
 
250
- # default id method to return the resource id when there is a
251
- # single key, and the model was defined with a primary key named
252
- # something other than id
347
+ # Destroy the instance, remove it from the repository
253
348
  #
254
- # ==== Returns
255
- # <Array[Key], Key> key or keys
349
+ # @return [Boolean]
350
+ # true if resource was destroyed
256
351
  #
257
- # --
258
352
  # @api public
259
- def id
260
- key = self.key
261
- key.first if key.size == 1
353
+ chainable do
354
+ def destroy
355
+ destroy!
356
+ end
262
357
  end
263
358
 
264
- def key
265
- key_properties.map do |property|
266
- original_values[property.name] || property.get!(self)
359
+ # Destroy the instance, remove it from the repository, bypassing hooks
360
+ #
361
+ # @return [Boolean]
362
+ # true if resource was destroyed
363
+ #
364
+ # @api public
365
+ def destroy!
366
+ if saved? && repository.delete(Collection.new(query, [ self ])) == 1
367
+ @collection.delete(self) if @collection
368
+ reset
369
+ freeze
370
+ true
371
+ else
372
+ false
267
373
  end
268
374
  end
269
375
 
270
- def readonly!
271
- @readonly = true
376
+ # Compares another Resource for equality
377
+ #
378
+ # Resource is equal to +other+ if they are the same object (identity)
379
+ # or if they are both of the *same model* and all of their attributes
380
+ # are equivalent
381
+ #
382
+ # @param [Resource] other
383
+ # the other Resource to compare with
384
+ #
385
+ # @return [Boolean]
386
+ # true if they are equal, false if not
387
+ #
388
+ # @api public
389
+ def eql?(other)
390
+ return true if equal?(other)
391
+ instance_of?(other.class) && cmp?(other, :eql?)
272
392
  end
273
393
 
274
- def readonly?
275
- @readonly == true
394
+ # Compares another Resource for equivalency
395
+ #
396
+ # Resource is equal to +other+ if they are the same object (identity)
397
+ # or if they are both of the *same base model* and all of their attributes
398
+ # are equivalent
399
+ #
400
+ # @param [Resource] other
401
+ # the other Resource to compare with
402
+ #
403
+ # @return [Boolean]
404
+ # true if they are equivalent, false if not
405
+ #
406
+ # @api public
407
+ def ==(other)
408
+ return true if equal?(other)
409
+ other.respond_to?(:model) &&
410
+ model.base_model.equal?(other.model.base_model) &&
411
+ cmp?(other, :==)
276
412
  end
277
413
 
278
- # save the instance to the data-store
414
+ # Compares two Resources to allow them to be sorted
279
415
  #
280
- # ==== Returns
281
- # <True, False>:: results of the save
416
+ # @param [Resource] other
417
+ # The other Resource to compare with
282
418
  #
283
- # @see DataMapper::Repository#save
419
+ # @return [Integer]
420
+ # Return 0 if Resources should be sorted as the same, -1 if the
421
+ # other Resource should be after self, and 1 if the other Resource
422
+ # should be before self
284
423
  #
285
- # --
286
- # #public
287
- def save(context = :default)
288
- # Takes a context, but does nothing with it. This is to maintain the
289
- # same API through out all of dm-more. dm-validations requires a
290
- # context to be passed
291
-
292
- associations_saved = false
293
- child_associations.each { |a| associations_saved |= a.save }
294
-
295
- saved = new_record? ? create : update
296
-
297
- if saved
298
- original_values.clear
424
+ # @api public
425
+ def <=>(other)
426
+ unless other.kind_of?(model.base_model)
427
+ raise ArgumentError, "Cannot compare a #{other.model} instance with a #{model} instance"
299
428
  end
429
+ cmp = 0
430
+ model.default_order(repository_name).each do |direction|
431
+ cmp = direction.get(self) <=> direction.get(other)
432
+ break if cmp != 0
433
+ end
434
+ cmp
435
+ end
300
436
 
301
- parent_associations.each { |a| associations_saved |= a.save }
302
-
303
- # We should return true if the model (or any of its associations)
304
- # were saved.
305
- (saved | associations_saved) == true
437
+ # Returns hash value of the object.
438
+ # Two objects with the same hash value assumed equal (using eql? method)
439
+ #
440
+ # DataMapper resources are equal when their models have the same hash
441
+ # and they have the same set of properties
442
+ #
443
+ # When used as key in a Hash or Hash subclass, objects are compared
444
+ # by eql? and thus hash value has direct effect on lookup
445
+ #
446
+ # @api private
447
+ def hash
448
+ key.hash
306
449
  end
307
450
 
308
- # destroy the instance, remove it from the repository
451
+ # Get a Human-readable representation of this Resource instance
452
+ #
453
+ # Foo.new #=> #<Foo name=nil updated_at=nil created_at=nil id=nil>
309
454
  #
310
- # ==== Returns
311
- # <True, False>:: results of the destruction
455
+ # @return [String]
456
+ # Human-readable representation of this Resource instance
312
457
  #
313
- # --
314
458
  # @api public
315
- def destroy
316
- return false if new_record?
317
- return false unless repository.delete(to_query)
318
-
319
- @new_record = true
320
- repository.identity_map(model).delete(key)
321
- original_values.clear
459
+ def inspect
460
+ # TODO: display relationship values
461
+ attrs = properties.map do |property|
462
+ value = if new? || property.loaded?(self)
463
+ property.get!(self).inspect
464
+ else
465
+ '<not loaded>'
466
+ end
322
467
 
323
- properties.each do |property|
324
- # We'll set the original value to nil as if we had a new record
325
- original_values[property.name] = nil if attribute_loaded?(property.name)
468
+ "#{property.instance_variable_name}=#{value}"
326
469
  end
327
470
 
328
- true
471
+ "#<#{model.name} #{attrs.join(' ')}>"
329
472
  end
330
473
 
331
- # Checks if the attribute has been loaded
332
- #
333
- # ==== Example
334
- #
335
- # class Foo
336
- # include DataMapper::Resource
337
- # property :name, String
338
- # property :description, Text, :lazy => false
339
- # end
474
+ # Hash of original values of attributes that have unsaved changes
340
475
  #
341
- # Foo.new.attribute_loaded?(:description) # will return false
476
+ # @return [Hash]
477
+ # original values of attributes that have unsaved changes
342
478
  #
343
- # --
344
- # @api public
345
- def attribute_loaded?(name)
346
- instance_variable_defined?(properties[name].instance_variable_name)
479
+ # @api semipublic
480
+ def original_attributes
481
+ @original_attributes ||= {}
347
482
  end
348
483
 
349
- # fetches all the names of the attributes that have been loaded,
350
- # even if they are lazy but have been called
351
- #
352
- # ==== Returns
353
- # Array[<Symbol>]:: names of attributes that have been loaded
354
- #
355
- # ==== Example
484
+ # Checks if an attribute has been loaded from the repository
356
485
  #
486
+ # @example
357
487
  # class Foo
358
488
  # include DataMapper::Resource
359
- # property :name, String
360
- # property :description, Text, :lazy => false
489
+ #
490
+ # property :name, String
491
+ # property :description, Text, :lazy => false
361
492
  # end
362
493
  #
363
- # Foo.new.loaded_attributes # returns [:name]
494
+ # Foo.new.attribute_loaded?(:description) #=> false
364
495
  #
365
- # --
366
- # @api public
367
- def loaded_attributes
368
- properties.map{|p| p.name if attribute_loaded?(p.name)}.compact
496
+ # @return [Boolean]
497
+ # true if ivar +name+ has been loaded
498
+ #
499
+ # @return [Boolean]
500
+ # true if ivar +name+ has been loaded
501
+ #
502
+ # @api private
503
+ def attribute_loaded?(name)
504
+ properties[name].loaded?(self)
369
505
  end
370
506
 
371
- # set of original values of properties
507
+ # Checks if an attribute has unsaved changes
372
508
  #
373
- # ==== Returns
374
- # Hash:: original values of properties
509
+ # @param [Symbol] name
510
+ # name of attribute to check for unsaved changes
375
511
  #
376
- # --
377
- # @api public
378
- def original_values
379
- @original_values ||= {}
512
+ # @return [Boolean]
513
+ # true if attribute has unsaved changes
514
+ #
515
+ # @api semipublic
516
+ def attribute_dirty?(name)
517
+ dirty_attributes.key?(properties[name])
380
518
  end
381
519
 
382
- # Hash of attributes that have been marked dirty
520
+ # Hash of attributes that have unsaved changes
383
521
  #
384
- # ==== Returns
385
- # Hash:: attributes that have been marked dirty
522
+ # @return [Hash]
523
+ # attributes that have unsaved changes
386
524
  #
387
- # --
388
- # @api private
525
+ # @api semipublic
389
526
  def dirty_attributes
390
527
  dirty_attributes = {}
391
- properties = self.properties
392
-
393
- original_values.each do |name, old_value|
394
- property = properties[name]
395
- new_value = property.get!(self)
396
-
397
- dirty = case property.track
398
- when :hash then old_value != new_value.hash
399
- else
400
- property.value(old_value) != property.value(new_value)
401
- end
402
528
 
403
- if dirty
404
- property.hash
405
- dirty_attributes[property] = property.value(new_value)
406
- end
529
+ original_attributes.each_key do |property|
530
+ dirty_attributes[property] = property.value(property.get!(self))
407
531
  end
408
532
 
409
533
  dirty_attributes
410
534
  end
411
535
 
412
- # Checks if the class is dirty
536
+ # Saves the resource
413
537
  #
414
- # ==== Returns
415
- # True:: returns if class is dirty
538
+ # @return [Boolean]
539
+ # true if the resource was successfully saved
416
540
  #
417
- # --
418
- # @api public
419
- def dirty?
420
- dirty_attributes.any?
541
+ # @api semipublic
542
+ def save_self(safe = true)
543
+ if safe
544
+ new? ? create_hook : update_hook
545
+ else
546
+ new? ? _create : _update
547
+ end
421
548
  end
422
549
 
423
- # Checks if the attribute is dirty
550
+ # Saves the parent resources
424
551
  #
425
- # ==== Parameters
426
- # name<Symbol>:: name of attribute
552
+ # @return [Boolean]
553
+ # true if the parents were successfully saved
427
554
  #
428
- # ==== Returns
429
- # True:: returns if attribute is dirty
555
+ # @api private
556
+ def save_parents(safe = true)
557
+ parent_relationships.all? do |relationship|
558
+ parent = relationship.get!(self)
559
+ if parent.dirty? ? parent.save_parents(safe) && parent.save_self(safe) : parent.saved?
560
+ relationship.set(self, parent) # set the FK values
561
+ end
562
+ end
563
+ end
564
+
565
+ # Saves the children resources
430
566
  #
431
- # --
432
- # @api public
433
- def attribute_dirty?(name)
434
- dirty_attributes.has_key?(properties[name])
567
+ # @return [Boolean]
568
+ # true if the children were successfully saved
569
+ #
570
+ # @api private
571
+ def save_children(safe = true)
572
+ child_relationships.all? do |relationship|
573
+ association = relationship.get!(self)
574
+ safe ? association.save : association.save!
575
+ end
435
576
  end
436
577
 
578
+ # Reset the Resource to a similar state as a new record:
579
+ # removes it from identity map and clears original property
580
+ # values (thus making all properties non dirty)
581
+ #
582
+ # @api private
583
+ def reset
584
+ @saved = false
585
+ identity_map.delete(key)
586
+ original_attributes.clear
587
+ self
588
+ end
589
+
590
+ # Gets a Collection with the current Resource instance as its only member
591
+ #
592
+ # @return [Collection, FalseClass]
593
+ # nil if this is a new record,
594
+ # otherwise a Collection with self as its only member
595
+ #
596
+ # @api private
437
597
  def collection
438
- @collection ||= if query = to_query
439
- Collection.new(query) { |c| c << self }
440
- end
598
+ return @collection if @collection || new? || frozen?
599
+ @collection = Collection.new(query, [ self ])
441
600
  end
442
601
 
443
- # Reload association and all child association
602
+ protected
603
+
604
+ # Method for hooking callbacks on resource creation
444
605
  #
445
- # ==== Returns
446
- # self:: returns the class itself
606
+ # @return [Boolean]
607
+ # true if the create was successful, false if not
447
608
  #
448
- # --
449
- # @api public
450
- def reload
451
- unless new_record?
452
- reload_attributes(*loaded_attributes)
453
- (parent_associations + child_associations).each { |association| association.reload }
454
- end
609
+ # @api private
610
+ def create_hook
611
+ _create
612
+ end
455
613
 
456
- self
614
+ # Method for hooking callbacks on resource updates
615
+ #
616
+ # @return [Boolean]
617
+ # true if the update was successful, false if not
618
+ #
619
+ # @api private
620
+ def update_hook
621
+ _update
457
622
  end
458
623
 
459
- # Reload specific attributes
624
+ private
625
+
626
+ # Initialize a new instance of this Resource using the provided values
460
627
  #
461
- # ==== Parameters
462
- # *attributes<Array[<Symbol>]>:: name of attribute
628
+ # @param [Hash] attributes
629
+ # attribute values to use for the new instance
463
630
  #
464
- # ==== Returns
465
- # self:: returns the class itself
631
+ # @return [Hash]
632
+ # attribute values used in the new instance
466
633
  #
467
- # --
468
634
  # @api public
469
- def reload_attributes(*attributes)
470
- unless attributes.empty? || new_record?
471
- collection.reload(:fields => attributes)
472
- end
635
+ def initialize(attributes = {}, &block) # :nodoc:
636
+ self.attributes = attributes
637
+ end
473
638
 
474
- self
639
+ # Returns name of the repository this object
640
+ # was loaded from
641
+ #
642
+ # @return [String]
643
+ # name of the repository this object was loaded from
644
+ #
645
+ # @api private
646
+ def repository_name
647
+ repository.name
475
648
  end
476
649
 
477
- # Checks if the model has been saved
650
+ # Gets this instance's Model's properties
478
651
  #
479
- # ==== Returns
480
- # True:: status if the model is new
652
+ # @return [Array(Property)]
653
+ # List of this Resource's Model's properties
481
654
  #
482
- # --
483
- # @api public
484
- def new_record?
485
- !defined?(@new_record) || @new_record
655
+ # @api private
656
+ def properties
657
+ model.properties(repository_name)
486
658
  end
487
659
 
488
- # all the attributes of the model
660
+ # Gets this instance's Model's relationships
489
661
  #
490
- # ==== Returns
491
- # Hash[<Symbol>]:: All the (non)-lazy attributes
662
+ # @return [Array(Associations::Relationship)]
663
+ # List of this instance's Model's Relationships
492
664
  #
493
- # --
494
- # @api public
495
- def attributes
496
- properties.map do |p|
497
- [p.name, send(p.getter)] if p.reader_visibility == :public
498
- end.compact.to_hash
665
+ # @api private
666
+ def relationships
667
+ model.relationships(repository_name)
499
668
  end
500
669
 
501
- # Mass assign of attributes
670
+ # Returns the identity map for the model from the repository
502
671
  #
503
- # ==== Parameters
504
- # value_hash <Hash[<Symbol>]>::
672
+ # @return [IdentityMap]
673
+ # identity map of repository this object was loaded from
505
674
  #
506
- # --
507
- # @api public
508
- def attributes=(values_hash)
509
- values_hash.each do |name, value|
510
- name = name.to_s.sub(/\?\z/, '')
675
+ # @api semipublic
676
+ def identity_map
677
+ repository.identity_map(model)
678
+ end
511
679
 
512
- if self.class.public_method_defined?(setter = "#{name}=")
513
- send(setter, value)
514
- else
515
- raise ArgumentError, "The property '#{name}' is not a public property."
516
- end
517
- end
680
+ # Fetches all the names of the attributes that have been loaded,
681
+ # even if they are lazy but have been called
682
+ #
683
+ # @return [Array<Property>]
684
+ # names of attributes that have been loaded
685
+ #
686
+ # @api private
687
+ def loaded_properties
688
+ properties.select { |property| property.loaded?(self) }
518
689
  end
519
690
 
520
- # Updates attributes and saves model
691
+ # Lazy loads attributes not yet loaded
521
692
  #
522
- # ==== Parameters
523
- # attributes<Hash> Attributes to be updated
524
- # keys<Symbol, String, Array> keys of Hash to update (others won't be updated)
693
+ # @param [Array<Property>] fields
694
+ # the properties to reload
525
695
  #
526
- # ==== Returns
527
- # <TrueClass, FalseClass> if model got saved or not
696
+ # @return [self]
528
697
  #
529
- #-
530
- # @api public
531
- def update_attributes(hash, *update_only)
532
- unless hash.is_a?(Hash)
533
- raise ArgumentError, "Expecting the first parameter of " +
534
- "update_attributes to be a hash; got #{hash.inspect}"
698
+ # @api private
699
+ def lazy_load(fields)
700
+ eager_load(fields - loaded_properties)
701
+ end
702
+
703
+ # Reloads specified attributes
704
+ #
705
+ # @param [Array<Property>] fields
706
+ # the properties to reload
707
+ #
708
+ # @return [Resource]
709
+ # the receiver, the current Resource instance
710
+ #
711
+ # @api private
712
+ def eager_load(fields)
713
+ unless fields.empty? || new?
714
+ collection.reload(:fields => fields)
535
715
  end
536
- loop_thru = update_only.empty? ? hash.keys : update_only
537
- loop_thru.each { |attr| send("#{attr}=", hash[attr]) }
538
- save
716
+
717
+ self
539
718
  end
540
719
 
541
- # TODO: add docs
542
- def to_query(query = {})
543
- model.to_query(repository, key, query) unless new_record?
720
+ # Gets a Query that will return this Resource instance
721
+ #
722
+ # @return [Query]
723
+ # Query that will retrieve this Resource instance
724
+ #
725
+ # @api private
726
+ def query
727
+ Query.new(repository, model, model.key_conditions(repository, key))
544
728
  end
545
729
 
546
- # TODO: add docs
730
+ # TODO: document
547
731
  # @api private
548
- def _dump(*)
549
- ivars = {}
732
+ def parent_relationships
733
+ parent_relationships = []
550
734
 
551
- # dump all the loaded properties
552
- properties.each do |property|
553
- next unless attribute_loaded?(property.name)
554
- ivars[property.instance_variable_name] = property.get!(self)
555
- end
735
+ relationships.each_value do |relationship|
736
+ next unless relationship.respond_to?(:resource_for) && relationship.loaded?(self)
737
+ next unless relationship.get(self)
556
738
 
557
- # dump ivars used internally
558
- %w[ @new_record @original_values @readonly @repository ].each do |name|
559
- ivars[name] = instance_variable_get(name)
739
+ parent_relationships << relationship
560
740
  end
561
741
 
562
- Marshal.dump(ivars)
742
+ parent_relationships
563
743
  end
564
744
 
565
- protected
745
+ # Returns loaded child relationships
746
+ #
747
+ # @return [Array<Associations::OneToMany::Relationship>]
748
+ # array of child relationships for which this resource is parent and is loaded
749
+ #
750
+ # @api private
751
+ def child_relationships
752
+ child_relationships = []
566
753
 
567
- def properties
568
- model.properties(repository.name)
569
- end
754
+ relationships.each_value do |relationship|
755
+ next unless relationship.respond_to?(:collection_for) && relationship.loaded?(self)
570
756
 
571
- def key_properties
572
- model.key(repository.name)
573
- end
757
+ association = relationship.get!(self)
758
+ next unless association.loaded? || association.head.any? || association.tail.any?
574
759
 
575
- def relationships
576
- model.relationships(repository.name)
760
+ child_relationships << relationship
761
+ end
762
+
763
+ many_to_many, other = child_relationships.partition do |relationship|
764
+ relationship.kind_of?(Associations::ManyToMany::Relationship)
765
+ end
766
+
767
+ many_to_many + other
577
768
  end
578
769
 
770
+ # Creates the resource with default values
771
+ #
772
+ # If resource is not dirty or a new (not yet saved),
773
+ # this method returns false
774
+ #
775
+ # On successful save identity map of the repository is
776
+ # updated
777
+ #
579
778
  # Needs to be a protected method so that it is hookable
580
- def create
779
+ #
780
+ # The primary purpose of this method is to allow before :create
781
+ # hooks to fire at a point just before/after resource creation
782
+ #
783
+ # @return [Boolean]
784
+ # true if the receiver was successfully created
785
+ #
786
+ # @api private
787
+ def _create
581
788
  # Can't create a resource that is not dirty and doesn't have serial keys
582
- return false if new_record? && !dirty? && !model.key.any? { |p| p.serial? }
789
+ return false if new? && !dirty?
790
+
583
791
  # set defaults for new resource
584
792
  properties.each do |property|
585
- next if attribute_loaded?(property.name)
586
- property.set(self, property.default_for(self))
793
+ unless property.serial? || property.loaded?(self)
794
+ property.set(self, property.default_for(self))
795
+ end
587
796
  end
588
797
 
589
- return false unless repository.create([ self ]) == 1
798
+ repository.create([ self ])
590
799
 
591
800
  @repository = repository
592
- @new_record = false
801
+ @saved = true
593
802
 
594
- repository.identity_map(model).set(key, self)
803
+ original_attributes.clear
804
+
805
+ identity_map[key] = self
595
806
 
596
807
  true
597
808
  end
598
809
 
599
- # Needs to be a protected method so that it is hookable
600
- def update
810
+ # Updates resource state
811
+ #
812
+ # The primary purpose of this method is to allow before :update
813
+ # hooks to fire at a point just before/after resource update whether
814
+ # it is the result of Resource#save, or using Resource#update
815
+ #
816
+ # @return [Boolean]
817
+ # true if the receiver was successfully created
818
+ #
819
+ # @api private
820
+ def _update
601
821
  dirty_attributes = self.dirty_attributes
602
- return true if dirty_attributes.empty?
603
- repository.update(dirty_attributes, to_query) == 1
604
- end
605
822
 
606
- private
607
-
608
- def initialize(attributes = {}) # :nodoc:
609
- assert_valid_model
610
- self.attributes = attributes
611
- end
612
-
613
- def assert_valid_model # :nodoc:
614
- return if self.class._valid_model
615
- properties = self.properties
823
+ if dirty_attributes.empty?
824
+ true
825
+ elsif dirty_attributes.any? { |property, value| !property.nullable? && value.nil? }
826
+ false
827
+ else
828
+ # remove from the identity map
829
+ identity_map.delete(key)
616
830
 
617
- if properties.empty? && relationships.empty?
618
- raise IncompleteResourceError, "#{model.name} must have at least one property or relationship to be initialized."
619
- end
831
+ return false unless repository.update(dirty_attributes, Collection.new(query, [ self ])) == 1
620
832
 
621
- if properties.key.empty?
622
- raise IncompleteResourceError, "#{model.name} must have a key."
623
- end
833
+ # remove the cached key in case it is updated
834
+ remove_instance_variable(:@key)
624
835
 
625
- self.class.instance_variable_set("@_valid_model", true)
626
- end
836
+ original_attributes.clear
627
837
 
628
- # TODO document
629
- # @api semipublic
630
- def attribute_get!(name)
631
- properties[name].get!(self)
632
- end
838
+ identity_map[key] = self
633
839
 
634
- # TODO document
635
- # @api semipublic
636
- def attribute_set!(name, value)
637
- properties[name].set!(self, value)
840
+ true
841
+ end
638
842
  end
639
843
 
640
- def lazy_load(name)
641
- reload_attributes(*properties.lazy_load_context(name) - loaded_attributes)
642
- end
844
+ # Return true if +other+'s is equivalent or equal to +self+'s
845
+ #
846
+ # @param [Resource] other
847
+ # The Resource whose attributes are to be compared with +self+'s
848
+ # @param [Symbol] operator
849
+ # The comparison operator to use to compare the attributes
850
+ #
851
+ # @return [Boolean]
852
+ # The result of the comparison of +other+'s attributes with +self+'s
853
+ #
854
+ # @api private
855
+ def cmp?(other, operator)
856
+ return false unless key.send(operator, other.key)
857
+ return true if repository.send(operator, other.repository) && !dirty? && !other.dirty?
643
858
 
644
- def child_associations
645
- @child_associations ||= []
646
- end
859
+ # get all the loaded and non-loaded properties that are not keys,
860
+ # since the key comparison was performed earlier
861
+ loaded, not_loaded = properties.select { |property| !property.key? }.partition do |property|
862
+ property.loaded?(self) && property.loaded?(other)
863
+ end
647
864
 
648
- def parent_associations
649
- @parent_associations ||= []
865
+ # check all loaded properties, and then all unloaded properties
866
+ (loaded + not_loaded).all? { |property| property.get(self).send(operator, property.get(other)) }
650
867
  end
651
868
 
652
- # TODO: move to dm-more/dm-transactions
653
- module Transaction
654
- # Produce a new Transaction for the class of this Resource
655
- #
656
- # ==== Returns
657
- # <DataMapper::Adapters::Transaction>::
658
- # a new DataMapper::Adapters::Transaction with all DataMapper::Repositories
659
- # of the class of this DataMapper::Resource added.
660
- #-
661
- # @api public
662
- #
663
- # TODO: move to dm-more/dm-transactions
664
- def transaction
665
- model.transaction { |*block_args| yield(*block_args) }
869
+ # Raises an exception if #update is performed on a dirty resource
870
+ #
871
+ # @param [Symbol] method
872
+ # the name of the method to use in the exception
873
+ #
874
+ # @return [undefined]
875
+ #
876
+ # @raise [UpdateConflictError]
877
+ # raise if the resource is dirty
878
+ #
879
+ # @api private
880
+ def assert_update_clean_only(method)
881
+ if original_attributes.any?
882
+ raise UpdateConflictError, "#{model}##{method} cannot be called on a dirty resource"
666
883
  end
667
- end # module Transaction
668
-
669
- include Transaction
884
+ end
670
885
  end # module Resource
671
886
  end # module DataMapper