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