mongodb-mongo_record 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,830 @@
1
+ #--
2
+ # Copyright (C) 2009 10gen Inc.
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify it
5
+ # under the terms of the GNU Affero General Public License, version 3, as
6
+ # published by the Free Software Foundation.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but WITHOUT
9
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
10
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
11
+ # for more details.
12
+ #
13
+ # You should have received a copy of the GNU Affero General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+ #++
16
+
17
+ require 'rubygems'
18
+ require 'mongo/types/objectid'
19
+ require 'mongo/cursor'
20
+ require 'mongo_record/convert'
21
+ require 'mongo_record/sql'
22
+
23
+ class String
24
+ # Convert this String to an ObjectID.
25
+ def to_oid
26
+ XGen::Mongo::Driver::ObjectID.from_string(self)
27
+ end
28
+ end
29
+
30
+ class XGen::Mongo::Driver::ObjectID
31
+ # Convert this object to an ObjectID.
32
+ def to_oid
33
+ self
34
+ end
35
+ end
36
+
37
+ module MongoRecord
38
+
39
+ class PKFactory
40
+ def create_pk(row)
41
+ return row if row[:_id]
42
+ row.delete(:_id) # in case it is nil
43
+ row['_id'] ||= XGen::Mongo::Driver::ObjectID.new
44
+ row
45
+ end
46
+ end
47
+
48
+ class MongoError < StandardError #:nodoc:
49
+ end
50
+ class PreparedStatementInvalid < MongoError #:nodoc:
51
+ end
52
+ class RecordNotFound < MongoError #:nodoc:
53
+ end
54
+ class RecordNotSaved < MongoError #:nodoc:
55
+ end
56
+
57
+ # A superclass for database collection instances. The API is very similar
58
+ # to ActiveRecord. See #find for examples.
59
+ #
60
+ # If you override initialize, make sure to call the superclass version,
61
+ # passing it the database row or hash that it was given.
62
+ #
63
+ # Example:
64
+ #
65
+ # class MP3Track < MongoRecord::Base
66
+ # collection_name :mp3_track
67
+ # fields :artist, :album, :song, :track
68
+ # def to_s
69
+ # "artist: #{self.artist}, album: #{self.album}, song: #{self.song}, track: #{track}"
70
+ # end
71
+ # end
72
+ #
73
+ # track = MP3Track.find_by_song('She Blinded Me With Science')
74
+ # puts track.to_s
75
+ #
76
+ # The database connection defaults to the global $db. You can set the
77
+ # connection using MongoRecord::Base.connection= and read it with
78
+ # MongoRecord::Base.connection.
79
+ #
80
+ # # Set the connection to something besides $db
81
+ # MongoRecord::Base.connection = connect('my-database')
82
+ class Base
83
+
84
+ @@connection = nil
85
+
86
+ class << self # Class methods
87
+
88
+ # Return the database connection. The default value is # <code>$db</code>.
89
+ def connection
90
+ conn = @@connection || $db
91
+ raise "connection not defined" unless conn
92
+ conn
93
+ end
94
+
95
+ # Set the database connection. If the connection is set to +nil+, then
96
+ # <code>$db</code> will be used.
97
+ def connection=(val)
98
+ @@connection = val
99
+ @@connection.pk_factory = PKFactory.new unless @@connection.pk_factory
100
+ end
101
+
102
+ # This method only exists so that MongoRecord::Base and
103
+ # ActiveRecord::Base can live side by side.
104
+ def instantiate(row={})
105
+ new(row)
106
+ end
107
+
108
+ # Get ready to save information about +subclass+.
109
+ def inherited(subclass)
110
+ subclass.instance_variable_set("@coll_name", class_name_to_field_name(subclass.name)) # default name
111
+ subclass.instance_variable_set("@field_names", []) # array of scalars names (symbols)
112
+ subclass.instance_variable_set("@subobjects", {}) # key = name (symbol), value = class
113
+ subclass.instance_variable_set("@arrays", {}) # key = name (symbol), value = class
114
+ end
115
+
116
+ # Call this method to set the Mongo collection name for this class.
117
+ # The default value is the class name turned into
118
+ # lower_case_with_underscores.
119
+ def collection_name(coll_name)
120
+ @coll_name = coll_name
121
+ field(:_id, :_ns, :_update)
122
+ end
123
+
124
+ # Creates one or more collection fields. Each field will be saved to
125
+ # and loaded from the database. The fields named "_id" and "_ns" are
126
+ # automatically saved and loaded.
127
+ #
128
+ # The method "field" is also called "fields"; you can use either one.
129
+ def field(*fields)
130
+ fields.each { |field|
131
+ field = field.to_sym
132
+ unless @field_names.include?(field)
133
+ ivar_name = "@" + field.to_s
134
+ define_method(field, lambda { instance_variable_get(ivar_name) })
135
+ define_method("#{field}=".to_sym, lambda { |val| instance_variable_set(ivar_name, val) })
136
+ define_method("#{field}?".to_sym, lambda {
137
+ val = instance_variable_get(ivar_name)
138
+ val != nil && (!val.kind_of?(String) || val != '')
139
+ })
140
+ @field_names << field
141
+ end
142
+ }
143
+ end
144
+ alias_method :fields, :field
145
+
146
+ # Return the field names.
147
+ def field_names; @field_names; end
148
+
149
+ # Return the names of all instance variables that hold objects
150
+ # declared using has_one. The names do not start with '@'.
151
+ #
152
+ # These are not necessarily MongoRecord::Subobject subclasses.
153
+ def subobjects; @subobjects; end
154
+
155
+ # Return the names of all instance variables that hold objects
156
+ # declared using has_many. The names do not start with '@'.
157
+ def arrays; @arrays; end
158
+
159
+ # Return the names of all fields, subobjects, and arrays.
160
+ def mongo_ivar_names; @field_names + @subobjects.keys + @arrays.keys; end
161
+
162
+ # Tell Mongo about a subobject (which need not be a
163
+ # MongoRecord::Subobject).
164
+ #
165
+ # Options:
166
+ # <code>:class_name<code> - Name of the class of the subobject.
167
+ def has_one(name, options={})
168
+ name = name.to_sym
169
+ unless @subobjects[name]
170
+ ivar_name = "@" + name.to_s
171
+ define_method(name, lambda { instance_variable_get(ivar_name) })
172
+ define_method("#{name}=".to_sym, lambda { |val| instance_variable_set(ivar_name, val) })
173
+ define_method("#{name}?".to_sym, lambda {
174
+ val = instance_variable_get(ivar_name)
175
+ val != nil && (!val.kind_of?(String) || val != '')
176
+ })
177
+ klass_name = options[:class_name] || field_name_to_class_name(name)
178
+ @subobjects[name] = Kernel.const_get(klass_name)
179
+ end
180
+ end
181
+
182
+ # Tells Mongo about an array of subobjects (which need not be
183
+ # MongoRecord::Subobjects).
184
+ #
185
+ # Options:
186
+ # <code>:class_name</code> - Name of the class of the subobject.
187
+ def has_many(name, options={})
188
+ name = name.to_sym
189
+ unless @arrays[name]
190
+ ivar_name = "@" + name.to_s
191
+ define_method(name, lambda { instance_variable_get(ivar_name) })
192
+ define_method("#{name}=".to_sym, lambda { |val| instance_variable_set(ivar_name, val) })
193
+ define_method("#{name}?".to_sym, lambda { !instance_variable_get(ivar_name).empty? })
194
+ klass_name = options[:class_name] || field_name_to_class_name(name)
195
+ @arrays[name] = Kernel.const_get(klass_name)
196
+ end
197
+ end
198
+
199
+ # Tells Mongo that this object has and many belongs to another object.
200
+ # A no-op.
201
+ def has_and_belongs_to_many(name, options={})
202
+ end
203
+
204
+ # Tells Mongo that this object belongs to another. A no-op.
205
+ def belongs_to(name, options={})
206
+ end
207
+
208
+ # The collection object for this class, which will be different for
209
+ # every subclass of MongoRecord::Base.
210
+ def collection
211
+ connection.collection(@coll_name.to_s)
212
+ end
213
+
214
+ # Find one or more database objects.
215
+ #
216
+ # * Find by id (a single id or an array of ids) returns one record or a Cursor.
217
+ #
218
+ # * Find :first returns the first record that matches the options used
219
+ # or nil if not found.
220
+ #
221
+ # * Find :all records; returns a Cursor that can iterate over raw
222
+ # records.
223
+ #
224
+ # Options:
225
+ #
226
+ # <code>:conditions</code> - Hash where key is field name and value is
227
+ # field value. Value may be a simple value like a string, number, or
228
+ # regular expression.
229
+ #
230
+ # <code>:select</code> - Single field name or list of field names. If
231
+ # not specified, all fields are returned. Names may be symbols or
232
+ # strings. The database always returns _id and _ns fields.
233
+ #
234
+ # <code>:order</code> - If a symbol, orders by that field in ascending
235
+ # order. If a string like "field1 asc, field2 desc, field3", then
236
+ # sorts those fields in the specified order (default is ascending). If
237
+ # an array, each element is either a field name or symbol (which will
238
+ # be sorted in ascending order) or a hash where key =isfield and value
239
+ # is 'asc' or 'desc' (case-insensitive), 1 or -1, or if any other value
240
+ # then true == 1 and false/nil == -1.
241
+ #
242
+ # <code>:limit</code> - Maximum number of records to return.
243
+ #
244
+ # <code>:offset</code> - Number of records to skip.
245
+ #
246
+ # <code>:where</code> - A string containing a JavaScript expression.
247
+ # This expression is run by the database server against each record
248
+ # found after the :conditions are run.
249
+ #
250
+ # Examples for find by id:
251
+ # Person.find("48e5307114f4abdf00dfeb86") # returns the object for this ID
252
+ # Person.find(["a_hex_id", "another_hex_id"]) # returns a Cursor over these two objects
253
+ # Person.find(["a_hex_id"]) # returns a Cursor over the object with this ID
254
+ # Person.find("a_hex_id", :conditions => "admin = 1", :order => "created_on DESC")
255
+ #
256
+ # Examples for find first:
257
+ # Person.find(:first) # returns the first object in the collection
258
+ # Person.find(:first, :conditions => ["user_name = ?", user_name])
259
+ # Person.find(:first, :order => "created_on DESC", :offset => 5)
260
+ # Person.find(:first, :order => {:created_on => -1}, :offset => 5) # same as previous example
261
+ #
262
+ # Examples for find all:
263
+ # Person.find(:all) # returns a Cursor over all objects in the collection
264
+ # Person.find(:all, :conditions => ["category = ?, category], :limit => 50)
265
+ # Person.find(:all, :offset => 10, :limit => 10)
266
+ # Person.find(:all, :select => :name) # Only returns name (and _id) fields
267
+ #
268
+ # Find_by_*
269
+ # Person.find_by_name_and_age("Spongebob", 42)
270
+ # Person.find_all_by_name("Fred")
271
+ #
272
+ # Mongo-specific example:
273
+ # Person.find(:all, :where => "this.address.city == 'New York' || this.age = 42")
274
+ #
275
+ # As a side note, the :order, :limit, and :offset options are passed
276
+ # on to the Cursor (after the :order option is rewritten to be a
277
+ # hash). So
278
+ # Person.find(:all, :offset => 10, :limit => 10, :order => :created_on)
279
+ # is the same as
280
+ # Person.find(:all).skip(10).limit(10).sort({:created_on => 1})
281
+ def find(*args)
282
+ options = extract_options_from_args!(args)
283
+ case args.first
284
+ when :first
285
+ find_initial(options)
286
+ when :all
287
+ find_every(options)
288
+ else
289
+ find_from_ids(args, options)
290
+ end
291
+ end
292
+
293
+ # Returns all records matching mql. Not yet implemented.
294
+ def find_by_mql(mql) # :nodoc:
295
+ raise "not implemented"
296
+ end
297
+ alias_method :find_by_sql, :find_by_mql
298
+
299
+ # Returns the number of matching records.
300
+ def count(options={})
301
+ criteria = criteria_from(options[:conditions]).merge!(where_func(options[:where]))
302
+ collection.count(criteria)
303
+ end
304
+
305
+ # Deletes the record with the given id from the collection.
306
+ def delete(id)
307
+ collection.remove({:_id => id})
308
+ end
309
+ alias_method :remove, :delete
310
+
311
+ # Load the object with +id+ and delete it.
312
+ def destroy(id)
313
+ id.is_a?(Array) ? id.each { |oid| destroy(oid) } : find(id).destroy
314
+ end
315
+
316
+ # Not yet implemented.
317
+ def update_all(updates, conditions = nil)
318
+ # TODO
319
+ raise "not yet implemented"
320
+ end
321
+
322
+ # Destroy all objects that match +conditions+. Warning: if
323
+ # +conditions+ is +nil+, all records in the collection will be
324
+ # destroyed.
325
+ def destroy_all(conditions = nil)
326
+ find(:all, :conditions => conditions).each { |object| object.destroy }
327
+ end
328
+
329
+ # Deletes all records that match +condition+, which can be a
330
+ # Mongo-style hash or an ActiveRecord-like hash. Examples:
331
+ # Person.destroy_all "name like '%fred%' # SQL WHERE clause
332
+ # Person.destroy_all ["name = ?", 'Fred'] # Rails condition
333
+ # Person.destroy_all {:name => 'Fred'} # Mongo hash
334
+ def delete_all(conditions=nil)
335
+ collection.remove(criteria_from(conditions))
336
+ end
337
+
338
+ # Creates, saves, and returns a new database object.
339
+ def create(values_hash)
340
+ object = self.new(values_hash)
341
+ object.save
342
+ object
343
+ end
344
+
345
+ # Finds the record from the passed +id+, instantly saves it with the passed +attributes+ (if the validation permits it),
346
+ # and returns it. If the save fails under validations, the unsaved object is still returned.
347
+ #
348
+ # The arguments may also be given as arrays in which case the update method is called for each pair of +id+ and
349
+ # +attributes+ and an array of objects is returned.
350
+ # =>
351
+ # Example of updating one record:
352
+ # Person.update(15, {:user_name => 'Samuel', :group => 'expert'})
353
+ #
354
+ # Example of updating multiple records:
355
+ # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} }
356
+ # Person.update(people.keys, people.values)
357
+ def update(id, attributes)
358
+ if id.is_a?(Array)
359
+ i = -1
360
+ id.collect { |id| i += 1; update(id, attributes[i]) }
361
+ else
362
+ object = find(id)
363
+ object.update_attributes(attributes)
364
+ object
365
+ end
366
+ end
367
+
368
+ # Handles find_* methods such as find_by_name, find_all_by_shoe_size,
369
+ # and find_or_create_by_name.
370
+ def method_missing(sym, *args)
371
+ if match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(sym.to_s)
372
+ find_how_many = ($1 == 'all_by') ? :all : :first
373
+ field_names = $2.split(/_and_/)
374
+ super unless all_fields_exist?(field_names)
375
+ search = search_from_names_and_values(field_names, args)
376
+ self.find(find_how_many, {:conditions => search}, *args[field_names.length..-1])
377
+ elsif match = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/.match(sym.to_s)
378
+ create = $1 == 'create'
379
+ field_names = $2.split(/_and_/)
380
+ super unless all_fields_exist?(field_names)
381
+ search = search_from_names_and_values(field_names, args)
382
+ row = self.find(:first, {:conditions => search})
383
+ return self.new(row) if row # found
384
+ obj = self.new(search.merge(args[field_names.length] || {})) # new object using search and remainder of args
385
+ obj.save if create
386
+ obj
387
+ else
388
+ super
389
+ end
390
+ end
391
+
392
+ private
393
+
394
+ def extract_options_from_args!(args)
395
+ args.last.is_a?(Hash) ? args.pop : {}
396
+ end
397
+
398
+ def find_initial(options)
399
+ criteria = criteria_from(options[:conditions]).merge!(where_func(options[:where]))
400
+ fields = fields_from(options[:select])
401
+ row = collection.find(criteria, :fields => fields, :limit => 1).next_object
402
+ (row.nil? || row['_id'] == nil) ? nil : self.new(row)
403
+ end
404
+
405
+ def find_every(options)
406
+ criteria = criteria_from(options[:conditions]).merge!(where_func(options[:where]))
407
+
408
+ find_options = {}
409
+ find_options[:fields] = fields_from(options[:select]) if options[:select]
410
+ find_options[:limit] = options[:limit].to_i if options[:limit]
411
+ find_options[:offset] = options[:offset].to_i if options[:offset]
412
+ find_options[:sort] = sort_by_from(options[:order]) if options[:order]
413
+
414
+ cursor = collection.find(criteria, find_options)
415
+
416
+ # Override cursor.next_object so it returns a new instance of this class
417
+ eval "def cursor.next_object; #{self.name}.new(super()); end"
418
+ cursor
419
+ end
420
+
421
+ def find_from_ids(ids, options)
422
+ ids = ids.to_a.flatten.compact.uniq
423
+ raise RecordNotFound, "Couldn't find #{name} without an ID" unless ids.length > 0
424
+
425
+ criteria = criteria_from(options[:conditions]).merge!(where_func(options[:where]))
426
+ criteria[:_id] = ids_clause(ids)
427
+ fields = fields_from(options[:select])
428
+
429
+ if ids.length == 1
430
+ row = collection.find(criteria, :fields => fields, :limit => 1).next_object
431
+ raise RecordNotFound, "Couldn't find #{name} with ID=#{ids[0]} #{criteria.inspect}" if row == nil || row.empty?
432
+ self.new(row)
433
+ else
434
+ find_options = {}
435
+ find_options[:fields] = fields if fields
436
+ find_options[:sort] = sort_by_from(options[:order]) if options[:order]
437
+
438
+ cursor = collection.find(criteria, find_options)
439
+
440
+ # Override cursor.next_object so it returns a new instance of this class
441
+ eval "def cursor.next_object; #{self.name}.new(super()); end"
442
+ cursor
443
+ end
444
+ end
445
+
446
+ def ids_clause(ids)
447
+ ids.length == 1 ? ids[0].to_oid : {'$in' => ids.collect{|id| id.to_oid}}
448
+ end
449
+
450
+ # Returns true if all field_names are in @field_names.
451
+ def all_fields_exist?(field_names)
452
+ field_names.collect! {|f| f == 'id' ? '_id' : f}
453
+ (field_names - @field_names.collect{|f| f.to_s}).empty?
454
+ end
455
+
456
+ # Returns a db search hash, given field_names and values.
457
+ def search_from_names_and_values(field_names, values)
458
+ h = {}
459
+ field_names.each_with_index { |iv, i| h[iv.to_s] = values[i] }
460
+ h
461
+ end
462
+
463
+ # Given a "SymbolOrStringLikeThis", return the string "symbol_or_string_like_this".
464
+ def class_name_to_field_name(name)
465
+ name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '')
466
+ end
467
+
468
+ # Given a "symbol_or_string_like_this", return the string "SymbolOrStringLikeThis".
469
+ def field_name_to_class_name(name)
470
+ name = name.to_s.dup.gsub(/_([a-z])/) {$1.upcase}
471
+ name[0,1] = name[0,1].upcase
472
+ name
473
+ end
474
+
475
+ protected
476
+
477
+ # Turns array, string, or hash conditions into something useable by Mongo.
478
+ # ["name='%s' and group_id='%s'", "foo'bar", 4] returns {:name => 'foo''bar', :group_id => 4}
479
+ # "name='foo''bar' and group_id='4'" returns {:name => 'foo''bar', :group_id => 4}
480
+ # { :name => "foo'bar", :group_id => 4 } returns the hash, modified for Mongo
481
+ def criteria_from(condition) # :nodoc:
482
+ case condition
483
+ when Array
484
+ criteria_from_array(condition)
485
+ when String
486
+ criteria_from_string(condition)
487
+ when Hash
488
+ criteria_from_hash(condition)
489
+ else
490
+ {}
491
+ end
492
+ end
493
+
494
+ # Substitutes values at the end of an array into the string at its
495
+ # start, sanitizing strings in the values. Then passes the string on
496
+ # to criteria_from_string.
497
+ def criteria_from_array(condition) # :nodoc:
498
+ str, *values = condition
499
+ sql = if values.first.kind_of?(Hash) and str =~ /:\w+/
500
+ replace_named_bind_variables(str, values.first)
501
+ elsif str.include?('?')
502
+ replace_bind_variables(str, values)
503
+ else
504
+ str % values.collect {|value| quote(value) }
505
+ end
506
+ criteria_from_string(sql)
507
+ end
508
+
509
+ # Turns a string into a Mongo search condition hash.
510
+ def criteria_from_string(sql) # :nodoc:
511
+ MongoRecord::SQL::Parser.parse_where(sql)
512
+ end
513
+
514
+ # Turns a hash that ActiveRecord would expect into one for Mongo.
515
+ def criteria_from_hash(condition) # :nodoc:
516
+ h = {}
517
+ condition.each { |k,v|
518
+ h[k] = case v
519
+ when Array
520
+ {'$in' => k == 'id' || k == '_id' ? v.collect{ |val| val.to_oid} : v} # if id, can't pass in string; must be ObjectID
521
+ when Range
522
+ {'$gte' => v.first, '$lte' => v.last}
523
+ else
524
+ k == 'id' || k == '_id' ? v.to_oid : v
525
+ end
526
+ }
527
+ h
528
+ end
529
+
530
+ # Returns a hash useable by Mongo for applying +func+ on the db server.
531
+ # +func+ must be +nil+ or a JavaScript expression or function in a
532
+ # string.
533
+ def where_func(func) # :nodoc:
534
+ func ? {'$where' => func} : {}
535
+ end
536
+
537
+ def replace_named_bind_variables(str, h) # :nodoc:
538
+ str.gsub(/:(\w+)/) do
539
+ match = $1.to_sym
540
+ if h.include?(match)
541
+ quoted_bind_var(h[match])
542
+ else
543
+ raise PreparedStatementInvalid, "missing value for :#{match} in #{str}" # TODO this gets swallowed in find()
544
+ end
545
+ end
546
+ end
547
+
548
+ def replace_bind_variables(str, values) # :nodoc:
549
+ raise "parameter count does not match value count" unless str.count('?') == values.length
550
+ bound = values.dup
551
+ str.gsub('?') { quoted_bind_var(bound.shift) }
552
+ end
553
+
554
+ def quoted_bind_var(val) # :nodoc:
555
+ case val
556
+ when Array
557
+ val.collect{|v| quote(v)}.join(',')
558
+ else
559
+ quote(val)
560
+ end
561
+ end
562
+
563
+ # Returns value quoted if appropriate (if it's a string).
564
+ def quote(val) # :nodoc:
565
+ return val unless val.is_a?(String)
566
+ return "'#{val.gsub(/\'/, "\\\\'")}'" # " <= for Emacs font-lock
567
+ end
568
+
569
+ def fields_from(a) # :nodoc:
570
+ return nil unless a
571
+ a = [a] unless a.kind_of?(Array)
572
+ a += ['_id'] # always return _id
573
+ a.uniq.collect { |k| k.to_s }
574
+ end
575
+
576
+ def sort_by_from(option) # :nodoc:
577
+ return nil unless option
578
+ sort_by = []
579
+ case option
580
+ when Symbol # Single value
581
+ sort_by << {option.to_s => 1}
582
+ when String
583
+ # TODO order these by building an array of hashes
584
+ fields = option.split(',')
585
+ fields.each {|f|
586
+ name, order = f.split
587
+ order ||= 'asc'
588
+ sort_by << {name.to_s => sort_value_from_arg(order)}
589
+ }
590
+ when Array # Array of field names; assume ascending sort
591
+ # TODO order these by building an array of hashes
592
+ sort_by = option.collect {|o| {o.to_s => 1}}
593
+ else # Hash (order of sorts is not guaranteed)
594
+ sort_by = option.collect {|k, v| {k.to_s => sort_value_from_arg(v)}}
595
+ end
596
+ return nil unless sort_by.length > 0
597
+ sort_by
598
+ end
599
+
600
+ # Turns "asc" into 1, "desc" into -1, and other values into 1 or -1.
601
+ def sort_value_from_arg(arg) # :nodoc:
602
+ case arg
603
+ when /^asc/i
604
+ arg = 1
605
+ when /^desc/i
606
+ arg = -1
607
+ when Number
608
+ arg.to_i >= 0 ? 1 : -1
609
+ else
610
+ arg ? 1 : -1
611
+ end
612
+ end
613
+
614
+ # Overwrite the default class equality method to provide support for association proxies.
615
+ def ===(object)
616
+ object.is_a?(self)
617
+ end
618
+
619
+ end # End of class methods
620
+
621
+ public
622
+
623
+ # Initialize a new object with either a hash of values or a row returned
624
+ # from the database.
625
+ def initialize(row={})
626
+ case row
627
+ when Hash
628
+ row.each { |k, val|
629
+ k = '_id' if k == 'id' # Rails helper
630
+ init_ivar("@#{k}", val)
631
+ }
632
+ else
633
+ row.instance_variables.each { |iv|
634
+ init_ivar(iv, row.instance_variable_get(iv))
635
+ }
636
+ end
637
+ # Default values for remaining fields
638
+ (self.class.field_names + self.class.subobjects.keys).each { |iv|
639
+ iv = "@#{iv}"
640
+ instance_variable_set(iv, nil) unless instance_variable_defined?(iv)
641
+ }
642
+ self.class.arrays.keys.each { |iv|
643
+ iv = "@#{iv}"
644
+ instance_variable_set(iv, []) unless instance_variable_defined?(iv)
645
+ }
646
+ yield self if block_given?
647
+ end
648
+
649
+ # Set the id of this object. Normally not called by user code.
650
+ def id=(val); @_id = (val == '' ? nil : val); end
651
+
652
+ # Return this object's id.
653
+ def id; @_id ? @_id.to_s : nil; end
654
+
655
+ # Return true if the +comparison_object+ is the same object, or is of
656
+ # the same type and has the same id.
657
+ def ==(comparison_object)
658
+ comparison_object.equal?(self) ||
659
+ (comparison_object.instance_of?(self.class) &&
660
+ comparison_object.id == id &&
661
+ !comparison_object.new_record?)
662
+ end
663
+
664
+ # Delegate to ==
665
+ def eql?(comparison_object)
666
+ self == (comparison_object)
667
+ end
668
+
669
+ # Delegate to id in order to allow two records of the same type and id to work with something like:
670
+ # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
671
+ def hash
672
+ id.hash
673
+ end
674
+
675
+ # Rails convenience method. Return this object's id as a string.
676
+ def to_param
677
+ @_id.to_s
678
+ end
679
+
680
+ # Save self and returns true if the save was successful, false if not.
681
+ def save
682
+ create_or_update
683
+ end
684
+
685
+ # Save self and returns true if the save was successful and raises
686
+ # RecordNotSaved if not.
687
+ def save!
688
+ create_or_update || raise(RecordNotSaved)
689
+ end
690
+
691
+ # Return true if this object is new---that is, does not yet have an id.
692
+ def new_record?
693
+ @_id == nil
694
+ end
695
+
696
+ # Convert this object to a Mongo value suitable for saving to the
697
+ # database.
698
+ def to_mongo_value
699
+ h = {}
700
+ self.class.mongo_ivar_names.each {|iv| h[iv] = instance_variable_get("@#{iv}").to_mongo_value }
701
+ h
702
+ end
703
+
704
+ # Save self to the database and set the id.
705
+ def create
706
+ set_create_times
707
+ with_id = self.class.collection.insert(to_mongo_value)
708
+ @_id = with_id['_id'] || with_id[:_id]
709
+ self
710
+ end
711
+
712
+ # Save self to the database. Return +false+ if there was an error,
713
+ # +self+ if all is well.
714
+ def update
715
+ set_update_times
716
+ row = self.class.collection.insert(to_mongo_value)
717
+ if row['_id'].to_s != @_id.to_s
718
+ return false
719
+ end
720
+ self
721
+ end
722
+
723
+ # Remove self from the database and set @_id to nil. If self has no
724
+ # @_id, does nothing.
725
+ def delete
726
+ if @_id
727
+ self.class.collection.remove({:_id => self._id})
728
+ @_id = nil
729
+ end
730
+ end
731
+ alias_method :remove, :delete
732
+
733
+ # Delete and freeze self.
734
+ def destroy
735
+ delete
736
+ freeze
737
+ end
738
+
739
+ #--
740
+ # ================================================================
741
+ # These methods exist so we can plug in ActiveRecord validation, etc.
742
+ # ================================================================
743
+ #++
744
+
745
+ # Updates a single attribute and saves the record. This is especially
746
+ # useful for boolean flags on existing records. Note: This method is
747
+ # overwritten by the Validation module that'll make sure that updates
748
+ # made with this method doesn't get subjected to validation checks.
749
+ # Hence, attributes can be updated even if the full object isn't valid.
750
+ def update_attribute(name, value)
751
+ send(name.to_s + '=', value)
752
+ save
753
+ end
754
+
755
+ # Updates all the attributes from the passed-in Hash and saves the
756
+ # record. If the object is invalid, the saving will fail and false will
757
+ # be returned.
758
+ def update_attributes(attributes)
759
+ self.attributes = attributes
760
+ save
761
+ end
762
+
763
+ # Updates an object just like Base.update_attributes but calls save!
764
+ # instead of save so an exception is raised if the record is invalid.
765
+ def update_attributes!(attributes)
766
+ self.attributes = attributes
767
+ save!
768
+ end
769
+
770
+ # Does nothing.
771
+ def attributes_from_column_definition; end
772
+
773
+ # ================================================================
774
+
775
+ private
776
+
777
+ def create_or_update
778
+ result = new_record? ? create : update
779
+ result != false
780
+ end
781
+
782
+ # Initialize ivar. +name+ must include the leading '@'.
783
+ def init_ivar(ivar_name, val)
784
+ sym = ivar_name[1..-1].to_sym
785
+ if self.class.subobjects.keys.include?(sym)
786
+ instance_variable_set(ivar_name, self.class.subobjects[sym].new(val))
787
+ elsif self.class.arrays.keys.include?(sym)
788
+ klazz = self.class.arrays[sym]
789
+ val = [val] unless val.kind_of?(Array)
790
+ instance_variable_set(ivar_name, val.collect {|v| v.kind_of?(MongoRecord::Base) ? v : klazz.new(v)})
791
+ else
792
+ instance_variable_set(ivar_name, val)
793
+ end
794
+ end
795
+
796
+ def set_create_times(t=nil)
797
+ t ||= Time.now
798
+ self.class.field_names.each { |iv|
799
+ case iv
800
+ when :created_at
801
+ instance_variable_set("@#{iv}", t)
802
+ when :created_on
803
+ instance_variable_set("@#{iv}", Time.local(t.year, t.month, t.day))
804
+ end
805
+ }
806
+ self.class.subobjects.keys.each { |iv|
807
+ val = instance_variable_get("@#{iv}")
808
+ val.send(:set_create_times, t) if val
809
+ }
810
+ end
811
+
812
+ def set_update_times(t=nil)
813
+ t ||= Time.now
814
+ self.class.field_names.each { |iv|
815
+ case iv
816
+ when :updated_at
817
+ instance_variable_set("@#{iv}", t)
818
+ when :updated_on
819
+ instance_variable_set("@#{iv}", Time.local(t.year, t.month, t.day))
820
+ end
821
+ }
822
+ self.class.subobjects.keys.each { |iv|
823
+ val = instance_variable_get("@#{iv}")
824
+ val.send(:set_update_times, t) if val
825
+ }
826
+ end
827
+
828
+ end
829
+
830
+ end