datamapper 0.2.5 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +5 -1
- data/FAQ +96 -0
- data/QUICKLINKS +12 -0
- data/README +57 -155
- data/environment.rb +61 -43
- data/example.rb +30 -12
- data/lib/data_mapper.rb +6 -1
- data/lib/data_mapper/adapters/abstract_adapter.rb +0 -57
- data/lib/data_mapper/adapters/data_object_adapter.rb +203 -97
- data/lib/data_mapper/adapters/mysql_adapter.rb +4 -0
- data/lib/data_mapper/adapters/postgresql_adapter.rb +7 -1
- data/lib/data_mapper/adapters/sql/coersion.rb +3 -2
- data/lib/data_mapper/adapters/sql/commands/load_command.rb +29 -10
- data/lib/data_mapper/adapters/sql/mappings/associations_set.rb +4 -0
- data/lib/data_mapper/adapters/sql/mappings/column.rb +13 -9
- data/lib/data_mapper/adapters/sql/mappings/conditions.rb +172 -0
- data/lib/data_mapper/adapters/sql/mappings/table.rb +43 -17
- data/lib/data_mapper/adapters/sqlite3_adapter.rb +9 -2
- data/lib/data_mapper/associations.rb +75 -3
- data/lib/data_mapper/associations/belongs_to_association.rb +70 -36
- data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +195 -86
- data/lib/data_mapper/associations/has_many_association.rb +168 -61
- data/lib/data_mapper/associations/has_n_association.rb +23 -3
- data/lib/data_mapper/attributes.rb +73 -0
- data/lib/data_mapper/auto_migrations.rb +2 -6
- data/lib/data_mapper/base.rb +5 -9
- data/lib/data_mapper/database.rb +4 -3
- data/lib/data_mapper/embedded_value.rb +66 -30
- data/lib/data_mapper/identity_map.rb +1 -3
- data/lib/data_mapper/is/tree.rb +121 -0
- data/lib/data_mapper/migration.rb +155 -0
- data/lib/data_mapper/persistence.rb +532 -218
- data/lib/data_mapper/property.rb +306 -0
- data/lib/data_mapper/query.rb +164 -0
- data/lib/data_mapper/support/blank.rb +2 -2
- data/lib/data_mapper/support/connection_pool.rb +5 -6
- data/lib/data_mapper/support/enumerable.rb +3 -3
- data/lib/data_mapper/support/errors.rb +10 -1
- data/lib/data_mapper/support/inflector.rb +174 -238
- data/lib/data_mapper/support/object.rb +54 -0
- data/lib/data_mapper/support/serialization.rb +19 -1
- data/lib/data_mapper/support/string.rb +7 -16
- data/lib/data_mapper/support/symbol.rb +3 -15
- data/lib/data_mapper/support/typed_set.rb +68 -0
- data/lib/data_mapper/types/base.rb +44 -0
- data/lib/data_mapper/types/string.rb +34 -0
- data/lib/data_mapper/validations/number_validator.rb +40 -0
- data/lib/data_mapper/validations/string_validator.rb +20 -0
- data/lib/data_mapper/validations/validator.rb +13 -0
- data/performance.rb +26 -1
- data/profile_data_mapper.rb +1 -1
- data/rakefile.rb +42 -2
- data/spec/acts_as_tree_spec.rb +11 -3
- data/spec/adapters/data_object_adapter_spec.rb +31 -0
- data/spec/associations/belongs_to_association_spec.rb +98 -0
- data/spec/associations/has_and_belongs_to_many_association_spec.rb +377 -0
- data/spec/associations/has_many_association_spec.rb +337 -0
- data/spec/attributes_spec.rb +23 -1
- data/spec/auto_migrations_spec.rb +86 -29
- data/spec/callbacks_spec.rb +107 -0
- data/spec/column_spec.rb +5 -2
- data/spec/count_command_spec.rb +33 -1
- data/spec/database_spec.rb +18 -0
- data/spec/dependency_spec.rb +4 -2
- data/spec/embedded_value_spec.rb +8 -8
- data/spec/fixtures/people.yaml +1 -1
- data/spec/fixtures/projects.yaml +10 -1
- data/spec/fixtures/tasks.yaml +6 -0
- data/spec/fixtures/tasks_tasks.yaml +2 -0
- data/spec/fixtures/tomatoes.yaml +1 -0
- data/spec/is_a_tree_spec.rb +149 -0
- data/spec/load_command_spec.rb +71 -9
- data/spec/magic_columns_spec.rb +17 -2
- data/spec/migration_spec.rb +267 -0
- data/spec/models/animal.rb +1 -1
- data/spec/models/candidate.rb +8 -0
- data/spec/models/career.rb +1 -1
- data/spec/models/chain.rb +8 -0
- data/spec/models/comment.rb +1 -1
- data/spec/models/exhibit.rb +1 -1
- data/spec/models/fence.rb +7 -0
- data/spec/models/fruit.rb +2 -2
- data/spec/models/job.rb +8 -0
- data/spec/models/person.rb +2 -3
- data/spec/models/post.rb +1 -1
- data/spec/models/project.rb +21 -1
- data/spec/models/section.rb +1 -1
- data/spec/models/serializer.rb +1 -1
- data/spec/models/task.rb +9 -0
- data/spec/models/tomato.rb +27 -0
- data/spec/models/user.rb +8 -2
- data/spec/models/zoo.rb +2 -7
- data/spec/paranoia_spec.rb +1 -1
- data/spec/{base_spec.rb → persistence_spec.rb} +207 -18
- data/spec/postgres_spec.rb +48 -6
- data/spec/property_spec.rb +90 -9
- data/spec/query_spec.rb +71 -5
- data/spec/save_command_spec.rb +11 -0
- data/spec/spec_helper.rb +14 -11
- data/spec/support/blank_spec.rb +8 -0
- data/spec/support/inflector_spec.rb +41 -0
- data/spec/support/object_spec.rb +9 -0
- data/spec/{serialization_spec.rb → support/serialization_spec.rb} +1 -1
- data/spec/support/silence_spec.rb +15 -0
- data/spec/{support_spec.rb → support/string_spec.rb} +3 -3
- data/spec/support/struct_spec.rb +12 -0
- data/spec/support/typed_set_spec.rb +66 -0
- data/spec/table_spec.rb +3 -3
- data/spec/types/string.rb +81 -0
- data/spec/validates_uniqueness_of_spec.rb +17 -0
- data/spec/validations/number_validator.rb +59 -0
- data/spec/validations/string_validator.rb +14 -0
- metadata +59 -17
- data/do_performance.rb +0 -153
- data/lib/data_mapper/support/active_record_impersonation.rb +0 -103
- data/lib/data_mapper/support/weak_hash.rb +0 -46
- data/spec/active_record_impersonation_spec.rb +0 -129
- data/spec/associations_spec.rb +0 -232
- data/spec/conditions_spec.rb +0 -49
- data/spec/has_many_association_spec.rb +0 -173
- data/spec/models/animals_exhibit.rb +0 -8
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'data_mapper/property'
|
2
|
-
require 'data_mapper/
|
2
|
+
require 'data_mapper/attributes'
|
3
3
|
require 'data_mapper/support/serialization'
|
4
4
|
require 'data_mapper/validations'
|
5
5
|
require 'data_mapper/associations'
|
@@ -12,129 +12,471 @@ require 'data_mapper/support/struct'
|
|
12
12
|
module DataMapper
|
13
13
|
# See DataMapper::Persistence::ClassMethods for DataMapper's DSL documentation.
|
14
14
|
module Persistence
|
15
|
+
|
16
|
+
class IncompleteModelDefinitionError < StandardError
|
17
|
+
end
|
18
|
+
|
15
19
|
# This probably needs to be protected
|
16
20
|
attr_accessor :loaded_set
|
17
|
-
|
21
|
+
|
22
|
+
include Comparable
|
23
|
+
|
18
24
|
def self.included(klass)
|
25
|
+
|
19
26
|
klass.extend(ClassMethods)
|
27
|
+
klass.extend(ConvenienceMethods::ClassMethods)
|
20
28
|
|
29
|
+
klass.send(:include, ConvenienceMethods::InstanceMethods)
|
30
|
+
klass.send(:include, Attributes)
|
21
31
|
klass.send(:include, Associations)
|
22
32
|
klass.send(:include, Validations)
|
23
33
|
klass.send(:include, CallbacksHelper)
|
24
|
-
klass.send(:include, Support::ActiveRecordImpersonation)
|
25
34
|
klass.send(:include, Support::Serialization)
|
26
35
|
|
27
|
-
prepare_for_persistence(klass)
|
28
|
-
end
|
29
|
-
|
30
|
-
def self.prepare_for_persistence(klass)
|
31
36
|
klass.instance_variable_set('@properties', [])
|
32
|
-
|
37
|
+
|
33
38
|
klass.send :extend, AutoMigrations
|
34
39
|
klass.subclasses
|
35
40
|
DataMapper::Persistence::subclasses << klass unless klass == DataMapper::Base
|
36
41
|
klass.send(:undef_method, :id) if method_defined?(:id)
|
37
42
|
|
38
|
-
return if klass == DataMapper::Base
|
39
|
-
|
40
43
|
# When this class is sub-classed, copy the declared columns.
|
41
44
|
klass.class_eval do
|
42
45
|
def self.subclasses
|
43
|
-
@subclasses || (@subclasses =
|
46
|
+
@subclasses || (@subclasses = Support::TypedSet.new(Class))
|
44
47
|
end
|
45
|
-
|
48
|
+
|
46
49
|
def self.inherited(subclass)
|
47
50
|
super_table = database.table(self)
|
48
|
-
|
51
|
+
|
49
52
|
if super_table.type_column.nil?
|
50
53
|
super_table.add_column(:type, :class, {})
|
51
54
|
end
|
52
|
-
|
55
|
+
|
56
|
+
subclass.instance_variable_set('@properties', self.instance_variable_get("@properties").dup)
|
53
57
|
subclass.instance_variable_set("@callbacks", self.callbacks.dup)
|
54
|
-
|
58
|
+
|
55
59
|
self::subclasses << subclass
|
56
60
|
end
|
57
|
-
|
61
|
+
|
58
62
|
def self.persistent?
|
59
63
|
true
|
60
64
|
end
|
61
65
|
end
|
62
66
|
end
|
63
67
|
|
68
|
+
# Migrates the database schema based on the properties defined within
|
69
|
+
# models. This includes removing fields no longer listed in models and
|
70
|
+
# adding new ones.
|
71
|
+
#
|
72
|
+
# This is destructive. Any data stored in the database will be destroyed
|
73
|
+
# when this method is called.
|
74
|
+
#
|
75
|
+
# ==== Returns
|
76
|
+
# True:: successfully automigrated database
|
77
|
+
# False:: an error occured when automigrating the database
|
78
|
+
#
|
79
|
+
# @public
|
64
80
|
def self.auto_migrate!
|
65
81
|
subclasses.each do |subclass|
|
66
82
|
subclass.auto_migrate!
|
67
83
|
end
|
68
84
|
end
|
69
|
-
|
85
|
+
|
86
|
+
|
87
|
+
# Drops all tables known by the schema
|
88
|
+
#
|
89
|
+
# ==== Returns
|
90
|
+
# True:: successfully automigrated database
|
91
|
+
# False:: an error occured when automigrating the database
|
92
|
+
#
|
93
|
+
# @public
|
94
|
+
def self.drop_all_tables!
|
95
|
+
database.adapter.schema.each do |table|
|
96
|
+
table.drop!
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
70
100
|
# Track classes that include this module.
|
101
|
+
# ==== Returns
|
102
|
+
# Support::TypedSet::
|
103
|
+
# contains classes that include or inherit from this module
|
104
|
+
#
|
105
|
+
# @semipublic
|
71
106
|
def self.subclasses
|
72
|
-
@subclasses || (@subclasses =
|
107
|
+
@subclasses || (@subclasses = Support::TypedSet.new(Class))
|
73
108
|
end
|
74
|
-
|
109
|
+
|
110
|
+
# Track classes that include this module.
|
111
|
+
# ==== Returns
|
112
|
+
# Support::TypedSet::
|
113
|
+
# contains classes that include or inherit from this module
|
114
|
+
#
|
115
|
+
# @semipublic
|
75
116
|
def self.dependencies
|
76
|
-
@dependency_queue || (@dependency_queue = DependencyQueue.new)
|
117
|
+
@dependency_queue || (@dependency_queue = DependencyQueue.new)
|
77
118
|
end
|
78
119
|
|
79
120
|
def initialize(details = nil)
|
121
|
+
check_for_properties!
|
122
|
+
if details
|
123
|
+
initialize_with_attributes(details)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def initialize_with_attributes(details)
|
80
128
|
case details
|
81
129
|
when Hash then self.attributes = details
|
82
|
-
when details.respond_to?(:persistent?) then self.
|
83
|
-
when Struct then self.
|
84
|
-
when NilClass then nil
|
130
|
+
when details.respond_to?(:persistent?) then self.private_attributes = details.attributes
|
131
|
+
when Struct then self.private_attributes = details.attributes
|
85
132
|
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def check_for_properties!
|
136
|
+
raise IncompleteModelDefinitionError.new("Models must have at least one property to be initialized.") if self.class.properties.empty?
|
86
137
|
end
|
87
|
-
|
138
|
+
|
139
|
+
module ConvenienceMethods
|
140
|
+
module InstanceMethods
|
141
|
+
|
142
|
+
# Save updated properties to the database.
|
143
|
+
#
|
144
|
+
# ==== Returns
|
145
|
+
# True:: successfully saved the object to the database
|
146
|
+
# False:: an error occured when saving the object to the database.
|
147
|
+
# check valid? to see if validation error occured
|
148
|
+
#
|
149
|
+
# @public
|
150
|
+
def save
|
151
|
+
database_context.save(self)
|
152
|
+
end
|
153
|
+
|
154
|
+
# This behaves in the same way as save, but raises a ValidationError
|
155
|
+
# if the model is invalid. Successful saves return true.
|
156
|
+
#
|
157
|
+
# ==== Returns
|
158
|
+
# True:: successfully saved the object to the database
|
159
|
+
#
|
160
|
+
# ==== Raises
|
161
|
+
# ValidationError::
|
162
|
+
# The object could not be saved to the database due to validation
|
163
|
+
# errors
|
164
|
+
# @public
|
165
|
+
def save!
|
166
|
+
raise ValidationError.new(errors) unless save
|
167
|
+
return true
|
168
|
+
end
|
169
|
+
|
170
|
+
# Reloads a model's properties from the database. This also includes
|
171
|
+
# data for any associated models that have been loaded from the
|
172
|
+
# database.
|
173
|
+
#
|
174
|
+
# You can limit the properties being reloaded by passing in an array
|
175
|
+
# of symbols.
|
176
|
+
def reload!(cols = nil)
|
177
|
+
database_context.first(self.class, key, :select => ([self.class.table.key.to_sym] + (cols || original_values.keys)).uniq, :reload => true)
|
178
|
+
self.loaded_associations.each { |association| association.reload! }
|
179
|
+
self
|
180
|
+
end
|
181
|
+
alias reload reload!
|
182
|
+
|
183
|
+
# Deletes the model from the database and de-activates associations
|
184
|
+
def destroy!
|
185
|
+
database_context.destroy(self)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
module ClassMethods
|
190
|
+
|
191
|
+
# Attempts to find an object using options passed as
|
192
|
+
# search_attributes, and falls back to creating the object if it
|
193
|
+
# can't find it.
|
194
|
+
#
|
195
|
+
# ==== Parameters
|
196
|
+
# search_attributes <hash>::
|
197
|
+
# attributes used to perform the search, and which can be later
|
198
|
+
# merged with create_attributes when creating a record
|
199
|
+
# create_attributes <hash>::
|
200
|
+
# attributes which are merged into the search_attributes when a
|
201
|
+
# record is unfound and needs to be created
|
202
|
+
#
|
203
|
+
# ==== Returns
|
204
|
+
# Object:: the found or created object from the database
|
205
|
+
#
|
206
|
+
# ==== Raises
|
207
|
+
# ValidationError::
|
208
|
+
# An object was not found, and could not be created due to errors
|
209
|
+
# in validation.
|
210
|
+
# DataObject::QueryError::
|
211
|
+
# The database threw an error
|
212
|
+
# -
|
213
|
+
# @public
|
214
|
+
def find_or_create(search_attributes, create_attributes = {})
|
215
|
+
first(search_attributes) || create(search_attributes.merge(create_attributes))
|
216
|
+
end
|
217
|
+
|
218
|
+
# returns an array of objects matching <tt>options</tt>.
|
219
|
+
#
|
220
|
+
# ==== Parameters
|
221
|
+
# options <hash>::
|
222
|
+
# hash of parameters to search by
|
223
|
+
#
|
224
|
+
# ==== Returns
|
225
|
+
# Array:: contains all matched objects from the database, or an
|
226
|
+
# empty set
|
227
|
+
#
|
228
|
+
# ==== Options
|
229
|
+
# Basics:
|
230
|
+
# Widget.all # => no conditions
|
231
|
+
# Widget.all :order => 'created_at desc' # => ORDER BY created_at desc
|
232
|
+
# Widget.all :limit => 10 # => LIMIT 10
|
233
|
+
# Widget.all :offset => 100 # => OFFSET 100
|
234
|
+
# Widget.all :include => [:gadgets] # => performs the JOIN according to
|
235
|
+
# its association with Gadgets
|
236
|
+
#
|
237
|
+
# Any non-standard options are assumed to be column names and are ANDed together:
|
238
|
+
# Widget.all :age => 10 # => WHERE age = 10
|
239
|
+
# Widget.all :age => 10, :title => 'Toy' # => WHERE age = 10 AND title = 'Toy'
|
240
|
+
#
|
241
|
+
# Using Symbol Operators[link:classes/DataMapper/Support/Symbol/Operator.html]:
|
242
|
+
# Widget.all :age.gt => 20 # => WHERE age > 10
|
243
|
+
# Widget.all :age.gte => 20, :name.like => '%Toy%' # => WHERE age >= 10 and name like '%Toy%'
|
244
|
+
#
|
245
|
+
# Variations of syntax include the :conditions => {} as well as interpolated arrays
|
246
|
+
# Widget.all :conditions => {:age => 10} # => WHERE age = 10
|
247
|
+
# Widget.all :conditions => ["age = ?", 10] # => WHERE age = 10
|
248
|
+
#
|
249
|
+
# Syntaxes can be mixed-and-matched as well
|
250
|
+
# Widget.all :conditions => ["age = ?", 10], :title => 'Toy'
|
251
|
+
# # => WHERE age = 10 AND title = 'Toy'
|
252
|
+
#
|
253
|
+
# ==== Raises
|
254
|
+
# DataMapper::Adapters::Sql::Commands::LoadCommand::ConditionsError::
|
255
|
+
# A query could not be constructed from the hash passed in as
|
256
|
+
# <tt>options</tt>
|
257
|
+
# DataObject::QueryError::
|
258
|
+
# The database threw an error
|
259
|
+
# -
|
260
|
+
# @public
|
261
|
+
def all(options = {})
|
262
|
+
database.all(self, options)
|
263
|
+
end
|
264
|
+
|
265
|
+
# Allows you to iterate over a collection of matching records. The
|
266
|
+
# first argument is the find options. The second is a block that will
|
267
|
+
# be called for every matching record.
|
268
|
+
#
|
269
|
+
# The valid options are the same as those documented in #all,
|
270
|
+
# except the <tt>:offset</tt> option, which is not allowed.
|
271
|
+
def each(options = {}, &b)
|
272
|
+
raise ArgumentError.new(":offset is not supported with the #each method") if options.has_key?(:offset)
|
273
|
+
|
274
|
+
offset = 0
|
275
|
+
limit = options[:limit] || (self::const_defined?('DEFAULT_LIMIT') ? self::DEFAULT_LIMIT : 500)
|
276
|
+
|
277
|
+
until (results = all(options.merge(:limit => limit, :offset => offset))).empty?
|
278
|
+
results.each(&b)
|
279
|
+
offset += limit
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Returns the first object which matches the query generated from the arguments
|
284
|
+
#
|
285
|
+
# ==== Parameters
|
286
|
+
# see all()
|
287
|
+
#
|
288
|
+
# ==== Returns
|
289
|
+
# Object:: first object from the database which matches the query
|
290
|
+
# nil:: no object could be found which matches the query
|
291
|
+
#
|
292
|
+
# ==== Raises
|
293
|
+
# DataMapper::Adapters::Sql::Commands::LoadCommand::ConditionsError::
|
294
|
+
# A query could not be generated from the arguments passed in
|
295
|
+
# DataObject::QueryError::
|
296
|
+
# The database threw an error
|
297
|
+
# -
|
298
|
+
# @public
|
299
|
+
def first(*args)
|
300
|
+
database.first(self, *args)
|
301
|
+
end
|
302
|
+
|
303
|
+
# returns the count of rows that match the given options hash. See
|
304
|
+
# all() for a list of possible arguments.
|
305
|
+
# NOTE: discards <tt>:offset</tt>, <tt>:limit</tt>, <tt>:order</tt>
|
306
|
+
#
|
307
|
+
# ==== Parameters
|
308
|
+
# see all().
|
309
|
+
#
|
310
|
+
# ==== Returns
|
311
|
+
# Integer:: number of rows matching query
|
312
|
+
#
|
313
|
+
# ==== Raises
|
314
|
+
# DataMapper::Adapters::Sql::Commands::LoadCommand::ConditionsError::
|
315
|
+
# A query could not be generated from the arguments passed in
|
316
|
+
# DataObject::QueryError::
|
317
|
+
# The database threw an error
|
318
|
+
# -
|
319
|
+
# @public
|
320
|
+
def count(*args)
|
321
|
+
database.count(self, *args)
|
322
|
+
end
|
323
|
+
|
324
|
+
# Does what it says. Deletes all records in a model's table.
|
325
|
+
# before_destroy and after_destroy callbacks are called and
|
326
|
+
# paranoia is respected.
|
327
|
+
#
|
328
|
+
# ==== Returns
|
329
|
+
# nil:: successfully deleted all rows
|
330
|
+
#
|
331
|
+
# ==== Raises
|
332
|
+
# DataObject::QueryError::
|
333
|
+
# The database threw an error
|
334
|
+
# -
|
335
|
+
# @public
|
336
|
+
def delete_all
|
337
|
+
database.delete_all(self)
|
338
|
+
end
|
339
|
+
|
340
|
+
def truncate!
|
341
|
+
database.truncate(self)
|
342
|
+
end
|
343
|
+
|
344
|
+
# This method allows for ActiveRecord style queries. The first
|
345
|
+
# argument is a symbol indicating a search for a single record or a
|
346
|
+
# collection — <tt>:first</tt> and <tt>:all</tt> respectively. The
|
347
|
+
# second argument is the hash of options for your query. For a list
|
348
|
+
# of valid options, please refer to the #all method.
|
349
|
+
#
|
350
|
+
# Widget.find(:all, :active => true) # => An array of active widgets
|
351
|
+
# Widget.find(:first, :active => true) # => The first active widget found
|
352
|
+
def find(type_or_id, options = {})
|
353
|
+
case type_or_id
|
354
|
+
when :first then first(options)
|
355
|
+
when :all then all(options)
|
356
|
+
else first(type_or_id, options)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
# supply this method with the full SQL you wish to search on, and it
|
361
|
+
# will return an array of Structs with your results set in them.
|
362
|
+
#
|
363
|
+
# NOTE: this does NOT return objects of a specific type, but rather
|
364
|
+
# Struct objects with as many attributes as what you requested in
|
365
|
+
# your full SQL query. These structs are read-only.
|
366
|
+
#
|
367
|
+
# If you only indicate you want 1 specific column, Datamapper and
|
368
|
+
# DataObjects will do their best to type-cast the result as best they
|
369
|
+
# can, rather than supplying you with an array of length 1 containing
|
370
|
+
# Structs with 1 attribute.
|
371
|
+
def find_by_sql(*args)
|
372
|
+
DataMapper::database.query(*args)
|
373
|
+
end
|
374
|
+
|
375
|
+
# finds a single row from the database by it's primary key.
|
376
|
+
# If you declared a property with <tt>:key => true</tt>, it's safe to
|
377
|
+
# use here.
|
378
|
+
# Example:
|
379
|
+
# Widget.get(100) # => widget with the primary key of 100
|
380
|
+
# Widget.get('Toy') # => widget with the primary natural key of 'Toy'
|
381
|
+
def get(*keys)
|
382
|
+
database.get(self, keys)
|
383
|
+
end
|
384
|
+
|
385
|
+
|
386
|
+
# synonym for get()
|
387
|
+
# ==== Parameters
|
388
|
+
# keys <any>:: keys which which to look up objects in the table.
|
389
|
+
#
|
390
|
+
# ==== Returns
|
391
|
+
# object :: object matching the request
|
392
|
+
#
|
393
|
+
# ==== Raises
|
394
|
+
# DataMapper::ObjectNotFoundError
|
395
|
+
# could not find the object requested
|
396
|
+
# -
|
397
|
+
# @public
|
398
|
+
def [](*keys)
|
399
|
+
# Eventually this ArgumentError should be removed. It's only here
|
400
|
+
# to help
|
401
|
+
# migrate users away from the [options_hash] syntax, which is no
|
402
|
+
# longer supported.
|
403
|
+
raise ArgumentError.new('Hash is not a valid key') if keys.size == 1 && keys.first.is_a?(Hash)
|
404
|
+
instance = database.get(self, keys)
|
405
|
+
raise ObjectNotFoundError.new() unless instance
|
406
|
+
return instance
|
407
|
+
end
|
408
|
+
|
409
|
+
# creates (and saves) a new instance of the object.
|
410
|
+
def create(attributes)
|
411
|
+
instance = self.new_with_attributes(attributes)
|
412
|
+
instance.save
|
413
|
+
instance
|
414
|
+
end
|
415
|
+
|
416
|
+
# the same as create(), though will raise an ObjectNotFoundError if
|
417
|
+
# the instance could not be saved
|
418
|
+
def create!(attributes)
|
419
|
+
instance = create(attributes)
|
420
|
+
raise ObjectNotFoundError.new(instance) if instance.new_record?
|
421
|
+
instance
|
422
|
+
end
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
88
426
|
module ClassMethods
|
427
|
+
|
428
|
+
def new_with_attributes(details)
|
429
|
+
instance = allocate
|
430
|
+
instance.initialize_with_attributes(details)
|
431
|
+
instance
|
432
|
+
end
|
89
433
|
|
90
434
|
# Track classes that include this module.
|
91
435
|
def subclasses
|
92
436
|
@subclasses || (@subclasses = [])
|
93
437
|
end
|
94
|
-
|
438
|
+
|
95
439
|
def logger
|
96
440
|
database.logger
|
97
441
|
end
|
98
|
-
|
442
|
+
|
99
443
|
def transaction
|
100
444
|
yield
|
101
445
|
end
|
102
|
-
|
446
|
+
|
447
|
+
# The foreign key for a model. It is based on the lowercased and
|
448
|
+
# underscored name of the class, suffixed with <tt>_id</tt>.
|
449
|
+
#
|
450
|
+
# Widget.foreign_key # => "widget_id"
|
451
|
+
# NewsItem.foreign_key # => "news_item_id"
|
103
452
|
def foreign_key
|
104
453
|
Inflector.underscore(self.name) + "_id"
|
105
454
|
end
|
106
455
|
|
107
456
|
def extended(klass)
|
108
|
-
unless klass == DataMapper::Base
|
109
|
-
klass.class_eval do
|
110
|
-
def persistent?
|
111
|
-
true
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
457
|
end
|
116
|
-
|
458
|
+
|
117
459
|
def table
|
118
460
|
database.table(self)
|
119
461
|
end
|
120
|
-
|
121
|
-
#
|
122
|
-
#
|
123
|
-
#
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
#
|
131
|
-
#
|
132
|
-
# to your model.
|
462
|
+
|
463
|
+
# Adds property accessors for a field that you'd like to be able to
|
464
|
+
# modify. The DataMapper doesn't
|
465
|
+
# use the table schema to infer accessors, you must explicity call
|
466
|
+
# #property to add field accessors
|
467
|
+
# to your model.
|
468
|
+
#
|
469
|
+
# Can accept an unlimited amount of property names. Optionally, you may
|
470
|
+
# pass the property names as an
|
471
|
+
# array.
|
472
|
+
#
|
473
|
+
# For more documentation, see Property.
|
133
474
|
#
|
134
475
|
# EXAMPLE:
|
135
476
|
# class CellProvider
|
136
477
|
# property :name, :string
|
137
|
-
# property :
|
478
|
+
# property :rating_number, :rating_percent, :integer # will create two properties with same type and text
|
479
|
+
# property [:bill_to, :ship_to, :mail_to], :text, :lazy => false # will create three properties all with same type and text
|
138
480
|
# end
|
139
481
|
#
|
140
482
|
# att = CellProvider.new(:name => 'AT&T')
|
@@ -146,46 +488,33 @@ module DataMapper
|
|
146
488
|
#
|
147
489
|
# OPTIONS:
|
148
490
|
# * <tt>lazy</tt>: Lazy load the specified property (:lazy => true). False by default.
|
149
|
-
# * <tt>accessor</tt>: Set method visibility for the property accessors. Affects both
|
150
|
-
# reader and writer. Allowable values are :public, :protected, :private. Defaults to
|
491
|
+
# * <tt>accessor</tt>: Set method visibility for the property accessors. Affects both
|
492
|
+
# reader and writer. Allowable values are :public, :protected, :private. Defaults to
|
151
493
|
# :public
|
152
494
|
# * <tt>reader</tt>: Like the accessor option but affects only the property reader.
|
153
495
|
# * <tt>writer</tt>: Like the accessor option but affects only the property writer.
|
154
496
|
# * <tt>protected</tt>: Alias for :reader => :public, :writer => :protected
|
155
497
|
# * <tt>private</tt>: Alias for :reader => :public, :writer => :private
|
156
|
-
def property(name, type, options = {})
|
157
498
|
|
158
|
-
|
159
|
-
|
160
|
-
|
499
|
+
def property(*columns_and_options)
|
500
|
+
columns, options = columns_and_options.partition {|item| not item.is_a?(Hash)}
|
501
|
+
options = (options.empty? ? {} : options[0])
|
502
|
+
type = columns.pop
|
161
503
|
|
162
|
-
|
163
|
-
|
164
|
-
writer_visibility = options[:writer] || options[:accessor] || :public
|
165
|
-
writer_visibility = :protected if options[:protected]
|
166
|
-
writer_visibility = :private if options[:private]
|
167
|
-
|
168
|
-
raise(ArgumentError.new, "property visibility must be :public, :protected, or :private") unless visibility_options.include?(reader_visibility) && visibility_options.include?(writer_visibility)
|
169
|
-
|
170
|
-
mapping = database.schema[self].add_column(name.to_s.sub(/\?$/, '').to_sym, type, options)
|
171
|
-
|
172
|
-
property_getter(mapping, reader_visibility)
|
173
|
-
property_setter(mapping, writer_visibility)
|
504
|
+
@properties ||= []
|
505
|
+
new_properties = []
|
174
506
|
|
175
|
-
|
176
|
-
|
507
|
+
columns.flatten.each do |name|
|
508
|
+
property = DataMapper::Property.new(self, name, type, options)
|
509
|
+
new_properties << property
|
510
|
+
@properties << property
|
177
511
|
end
|
178
|
-
|
179
|
-
return
|
512
|
+
|
513
|
+
return (new_properties.length == 1 ? new_properties[0] : new_properties)
|
180
514
|
end
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
:updated_on => lambda { before_save { |x| x.updated_on = Date::today } },
|
185
|
-
:created_at => lambda { before_create { |x| x.created_at = Time::now } },
|
186
|
-
:created_on => lambda { before_create { |x| x.created_on = Date::today } }
|
187
|
-
}
|
188
|
-
|
515
|
+
|
516
|
+
# TODO: Figure out how to make EmbeddedValue work with new property
|
517
|
+
# code. EV relies on these next two methods.
|
189
518
|
def property_getter(mapping, visibility = :public)
|
190
519
|
if mapping.lazy?
|
191
520
|
class_eval <<-EOS
|
@@ -201,15 +530,15 @@ module DataMapper
|
|
201
530
|
else
|
202
531
|
class_eval("#{visibility.to_s}; def #{mapping.name}; #{mapping.instance_variable_name} end") unless [ :public, :private, :protected ].include?(mapping.name)
|
203
532
|
end
|
204
|
-
|
533
|
+
|
205
534
|
if mapping.type == :boolean
|
206
535
|
class_eval("#{visibility.to_s}; def #{mapping.name.to_s.ensure_ends_with('?')}; #{mapping.instance_variable_name} end")
|
207
536
|
end
|
208
|
-
|
537
|
+
|
209
538
|
rescue SyntaxError
|
210
539
|
raise SyntaxError.new(mapping)
|
211
540
|
end
|
212
|
-
|
541
|
+
|
213
542
|
def property_setter(mapping, visibility = :public)
|
214
543
|
if mapping.lazy?
|
215
544
|
class_eval <<-EOS
|
@@ -227,7 +556,7 @@ module DataMapper
|
|
227
556
|
rescue SyntaxError
|
228
557
|
raise SyntaxError.new(mapping)
|
229
558
|
end
|
230
|
-
|
559
|
+
|
231
560
|
# Allows you to override the table name for a model.
|
232
561
|
# EXAMPLE:
|
233
562
|
# class WorkItem
|
@@ -236,10 +565,13 @@ module DataMapper
|
|
236
565
|
def set_table_name(value)
|
237
566
|
database.table(self).name = value
|
238
567
|
end
|
239
|
-
|
240
|
-
#
|
568
|
+
|
569
|
+
# An embedded value maps the values of an object to fields in the
|
570
|
+
# record of the object's owner.
|
571
|
+
# #embed takes a symbol to define the embedded class, options, and
|
572
|
+
# an optional block. See
|
241
573
|
# examples for use cases.
|
242
|
-
#
|
574
|
+
#
|
243
575
|
# EXAMPLE:
|
244
576
|
# class CellPhone < DataMapper::Base
|
245
577
|
# property :number, :string
|
@@ -257,30 +589,42 @@ module DataMapper
|
|
257
589
|
# => Nick
|
258
590
|
#
|
259
591
|
# OPTIONS:
|
260
|
-
#
|
261
|
-
#
|
262
|
-
#
|
263
|
-
#
|
264
|
-
#
|
265
|
-
#
|
266
|
-
#
|
267
|
-
#
|
268
|
-
#
|
269
|
-
#
|
270
|
-
#
|
271
|
-
#
|
592
|
+
# * <tt>prefix</tt>: define a column prefix, so instead of mapping
|
593
|
+
# :address to an 'address' column, it would map to
|
594
|
+
# 'owner_address' in the example above. If
|
595
|
+
# :prefix => true is specified, the prefix will
|
596
|
+
# be the name of the symbol given as the first
|
597
|
+
# parameter. If the prefix is a string the
|
598
|
+
# specified
|
599
|
+
# string will be used for the prefix.
|
600
|
+
# * <tt>lazy</tt>: lazy-load all embedded values at the same time.
|
601
|
+
# :lazy => true to enable. Disabled (false) by
|
602
|
+
# default.
|
603
|
+
# * <tt>accessor</tt>: Set method visibility for all embedded
|
604
|
+
# properties. Affects both reader and writer.
|
605
|
+
# Allowable values are :public, :protected,
|
606
|
+
# :private. Defaults to :public
|
607
|
+
# * <tt>reader</tt>: Like the accessor option but affects only
|
608
|
+
# embedded property readers.
|
609
|
+
# * <tt>writer</tt>: Like the accessor option but affects only
|
610
|
+
# embedded property writers.
|
611
|
+
# * <tt>protected</tt>: Alias for :reader => :public,
|
612
|
+
# :writer => :protected
|
613
|
+
# * <tt>private</tt>: Alias for :reader => :public, :writer => :private
|
272
614
|
#
|
273
615
|
def embed(name, options = {}, &block)
|
274
616
|
EmbeddedValue::define(self, name, options, &block)
|
275
|
-
end
|
276
|
-
|
617
|
+
end
|
618
|
+
|
619
|
+
# Returns the hash of properties for this model.
|
277
620
|
def properties
|
278
621
|
@properties
|
279
622
|
end
|
280
623
|
|
281
|
-
# Creates a composite index for an arbitrary number of database columns.
|
282
|
-
# it also is possible to specify single indexes directly for
|
283
|
-
#
|
624
|
+
# Creates a composite index for an arbitrary number of database columns.
|
625
|
+
# Note that it also is possible to specify single indexes directly for
|
626
|
+
# each property.
|
627
|
+
#
|
284
628
|
# === EXAMPLE WITH COMPOSITE INDEX:
|
285
629
|
# class Person < DataMapper::Base
|
286
630
|
# property :server_id, :integer
|
@@ -301,21 +645,23 @@ module DataMapper
|
|
301
645
|
# * property :name, :index => true
|
302
646
|
# * property :name, :index => :unique
|
303
647
|
def index(indexes, unique = false)
|
304
|
-
if indexes.kind_of?(Array) # if given an index of multiple columns
|
648
|
+
if indexes.kind_of?(Array) # if given an index of multiple columns
|
305
649
|
database.schema[self].add_composite_index(indexes, unique)
|
306
650
|
else
|
307
651
|
raise ArgumentError.new("You must supply an array for the composite index")
|
308
652
|
end
|
309
653
|
end
|
310
|
-
|
654
|
+
|
311
655
|
end
|
312
|
-
|
313
|
-
# Lazy-loads the attributes for a loaded_set, then overwrites the
|
314
|
-
#
|
656
|
+
|
657
|
+
# Lazy-loads the attributes for a loaded_set, then overwrites the
|
658
|
+
# accessors
|
659
|
+
# for the named methods so that the lazy_loading is skipped the second
|
660
|
+
# time.
|
315
661
|
def lazy_load!(*names)
|
316
|
-
|
662
|
+
|
317
663
|
names = names.map { |name| name.to_sym }.reject { |name| lazy_loaded_attributes.include?(name) }
|
318
|
-
|
664
|
+
|
319
665
|
reset_attribute = lambda do |instance|
|
320
666
|
singleton_class = (class << instance; self end)
|
321
667
|
names.each do |name|
|
@@ -323,14 +669,14 @@ module DataMapper
|
|
323
669
|
singleton_class.send(:attr_accessor, name)
|
324
670
|
end
|
325
671
|
end
|
326
|
-
|
672
|
+
|
327
673
|
unless names.empty? || new_record? || loaded_set.nil?
|
328
|
-
|
674
|
+
|
329
675
|
key = database_context.table(self.class).key.to_sym
|
330
676
|
keys_to_select = loaded_set.map do |instance|
|
331
677
|
instance.send(key)
|
332
678
|
end
|
333
|
-
|
679
|
+
|
334
680
|
database_context.all(
|
335
681
|
self.class,
|
336
682
|
:select => ([key] + names),
|
@@ -342,25 +688,10 @@ module DataMapper
|
|
342
688
|
end
|
343
689
|
end
|
344
690
|
|
345
|
-
# Mass-assign mapped fields.
|
346
|
-
def attributes=(values_hash)
|
347
|
-
table = database_context.table(self.class)
|
348
|
-
|
349
|
-
values_hash.delete_if do |key, value|
|
350
|
-
!self.class.public_method_defined?("#{key}=")
|
351
|
-
end.each_pair do |key, value|
|
352
|
-
if respond_to?(key)
|
353
|
-
send("#{key}=", value)
|
354
|
-
elsif column = table[key]
|
355
|
-
instance_variable_set(column.instance_variable_name, value)
|
356
|
-
end
|
357
|
-
end
|
358
|
-
end
|
359
|
-
|
360
691
|
def database_context
|
361
692
|
@database_context || ( @database_context = database )
|
362
693
|
end
|
363
|
-
|
694
|
+
|
364
695
|
def database_context=(value)
|
365
696
|
@database_context = value
|
366
697
|
end
|
@@ -368,77 +699,32 @@ module DataMapper
|
|
368
699
|
def logger
|
369
700
|
self.class.logger
|
370
701
|
end
|
371
|
-
|
702
|
+
|
703
|
+
# Returns <tt>true</tt> if this model hasn't been saved to the
|
704
|
+
# database, <tt>false</tt> otherwise.
|
372
705
|
def new_record?
|
373
706
|
@new_record.nil? || @new_record
|
374
707
|
end
|
375
708
|
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
# Returns the difference between two objects, in terms of their attributes.
|
381
|
-
def ^(other)
|
382
|
-
results = {}
|
383
|
-
|
384
|
-
self_attributes, other_attributes = attributes, other.attributes
|
385
|
-
|
386
|
-
self_attributes.each_pair do |k,v|
|
387
|
-
other_value = other_attributes[k]
|
388
|
-
unless v == other_value
|
389
|
-
results[k] = [v, other_value]
|
390
|
-
end
|
391
|
-
end
|
392
|
-
|
393
|
-
results
|
394
|
-
end
|
395
|
-
|
709
|
+
# Returns a Set containing the properties that have had their
|
710
|
+
# <tt>:lazy</tt> option set to true, or are lazily loaded by
|
711
|
+
# default — i.e. text fields.
|
396
712
|
def lazy_loaded_attributes
|
397
713
|
@lazy_loaded_attributes || @lazy_loaded_attributes = Set.new
|
398
714
|
end
|
399
|
-
|
400
|
-
|
401
|
-
pairs = {}
|
402
|
-
|
403
|
-
database_context.table(self).columns.each do |column|
|
404
|
-
pairs[column.name] = instance_variable_get(column.instance_variable_name)
|
405
|
-
end
|
406
|
-
|
407
|
-
pairs
|
408
|
-
end
|
409
|
-
|
715
|
+
|
716
|
+
# Accepts a hash of properties and values to be updated and then calls #save
|
410
717
|
def update_attributes(update_hash)
|
411
718
|
self.attributes = update_hash
|
412
719
|
self.save
|
413
720
|
end
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
value = instance_variable_get(column.instance_variable_name)
|
422
|
-
pairs[column.name] = column.type == :class ? value.to_s : value
|
423
|
-
end
|
424
|
-
end
|
425
|
-
|
426
|
-
pairs
|
427
|
-
end
|
428
|
-
|
429
|
-
def unsafe_attributes=(values_hash)
|
430
|
-
table = database_context.table(self.class)
|
431
|
-
|
432
|
-
values_hash.each_pair do |key, value|
|
433
|
-
if respond_to?(key)
|
434
|
-
send("#{key}=", value)
|
435
|
-
elsif column = table[key]
|
436
|
-
instance_variable_set(column.instance_variable_name, value)
|
437
|
-
end
|
438
|
-
end
|
439
|
-
end
|
440
|
-
|
441
|
-
def dirty?
|
721
|
+
|
722
|
+
# Returns <tt>true</tt> if the unsaved model has had properties changed
|
723
|
+
# since it was loaded from the database. Returns <tt>false</tt> otherwise.
|
724
|
+
def dirty?(cleared = Set.new)
|
725
|
+
return false if cleared.include?(self)
|
726
|
+
cleared << self
|
727
|
+
|
442
728
|
result = database_context.table(self).columns.any? do |column|
|
443
729
|
if column.type == :object
|
444
730
|
Marshal.dump(self.instance_variable_get(column.instance_variable_name)) != original_values[column.name]
|
@@ -446,72 +732,74 @@ module DataMapper
|
|
446
732
|
self.instance_variable_get(column.instance_variable_name) != original_values[column.name]
|
447
733
|
end
|
448
734
|
end
|
449
|
-
|
735
|
+
|
450
736
|
return true if result
|
451
|
-
|
737
|
+
|
452
738
|
loaded_associations.any? do |loaded_association|
|
453
|
-
loaded_association.dirty?
|
739
|
+
loaded_association.dirty?(cleared)
|
454
740
|
end
|
455
741
|
end
|
456
742
|
|
743
|
+
# For unsaved models, returns a hash of properties that have had their
|
744
|
+
# values changed since it was loaded from the database.
|
457
745
|
def dirty_attributes
|
458
746
|
pairs = {}
|
459
|
-
|
747
|
+
|
460
748
|
database_context.table(self).columns.each do |column|
|
461
749
|
value = instance_variable_get(column.instance_variable_name)
|
462
750
|
if value != original_values[column.name] && (!new_record? || !column.serial?)
|
463
751
|
pairs[column.name] = column.type != :object ? value : YAML.dump(value)
|
464
752
|
end
|
465
753
|
end
|
466
|
-
|
754
|
+
|
467
755
|
pairs
|
468
756
|
end
|
469
|
-
|
757
|
+
|
470
758
|
def original_values=(values)
|
471
759
|
values.each_pair do |k,v|
|
472
760
|
original_values[k] = case v
|
473
|
-
|
474
|
-
|
475
|
-
|
761
|
+
when String, Date, Time then v.dup
|
762
|
+
# when column.type == :object then Marshal.dump(v)
|
763
|
+
else v
|
476
764
|
end
|
477
765
|
end
|
478
766
|
end
|
479
|
-
|
767
|
+
|
480
768
|
def original_values
|
481
769
|
class << self
|
482
770
|
attr_reader :original_values
|
483
771
|
end
|
484
|
-
|
772
|
+
|
485
773
|
@original_values = {}
|
486
774
|
end
|
487
|
-
|
775
|
+
|
488
776
|
def loaded_set=(value)
|
489
777
|
value << self
|
490
778
|
@loaded_set = value
|
491
779
|
end
|
492
|
-
|
780
|
+
|
493
781
|
def inspect
|
494
782
|
inspected_attributes = attributes.map { |k,v| "@#{k}=#{v.inspect}" }
|
495
|
-
|
783
|
+
|
496
784
|
instance_variables.each do |name|
|
497
785
|
if instance_variable_get(name).kind_of?(Associations::HasManyAssociation)
|
498
786
|
inspected_attributes << "#{name}=#{instance_variable_get(name).inspect}"
|
499
787
|
end
|
500
788
|
end
|
501
|
-
|
789
|
+
|
502
790
|
"#<%s:0x%x @new_record=%s, %s>" % [self.class.name, (object_id * 2), new_record?, inspected_attributes.join(', ')]
|
503
791
|
end
|
504
|
-
|
792
|
+
|
505
793
|
def loaded_associations
|
506
794
|
@loaded_associations || @loaded_associations = []
|
507
795
|
end
|
508
|
-
|
796
|
+
|
509
797
|
def key=(value)
|
510
798
|
key_column = database_context.table(self.class).key
|
511
799
|
@__key = key_column.type_cast_value(value)
|
512
800
|
instance_variable_set(key_column.instance_variable_name, @__key)
|
513
801
|
end
|
514
|
-
|
802
|
+
|
515
803
|
def key
|
516
804
|
@__key || @__key = begin
|
517
805
|
key_column = database_context.table(self.class).key
|
@@ -519,20 +807,46 @@ module DataMapper
|
|
519
807
|
end
|
520
808
|
end
|
521
809
|
|
522
|
-
|
810
|
+
def keys
|
811
|
+
self.class.table.keys.map do |column|
|
812
|
+
column.type_cast_value(instance_variable_get(column.instance_variable_name))
|
813
|
+
end.compact
|
814
|
+
end
|
523
815
|
|
524
|
-
|
525
|
-
|
526
|
-
|
816
|
+
def <=>(other)
|
817
|
+
keys <=> other.keys
|
818
|
+
end
|
527
819
|
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
820
|
+
# Look to ::included for __hash alias
|
821
|
+
def hash
|
822
|
+
@__hash || @__hash = keys.empty? ? super : keys.hash
|
823
|
+
end
|
824
|
+
|
825
|
+
def eql?(other)
|
826
|
+
return false unless other.is_a?(self.class) || self.is_a?(other.class)
|
827
|
+
comparator = keys.empty? ? :private_attributes : :keys
|
828
|
+
send(comparator) == other.send(comparator)
|
829
|
+
end
|
830
|
+
|
831
|
+
def ==(other)
|
832
|
+
eql?(other)
|
833
|
+
end
|
834
|
+
|
835
|
+
# Returns the difference between two objects, in terms of their
|
836
|
+
# attributes.
|
837
|
+
def ^(other)
|
838
|
+
results = {}
|
839
|
+
|
840
|
+
self_attributes, other_attributes = attributes, other.attributes
|
841
|
+
|
842
|
+
self_attributes.each_pair do |k,v|
|
843
|
+
other_value = other_attributes[k]
|
844
|
+
unless v == other_value
|
845
|
+
results[k] = [v, other_value]
|
846
|
+
end
|
532
847
|
end
|
533
848
|
|
534
|
-
|
849
|
+
results
|
535
850
|
end
|
536
|
-
|
537
851
|
end
|
538
|
-
end
|
852
|
+
end
|