datamapper 0.2.5 → 0.3.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.
- 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
|