mongodb-mongo-activerecord-ruby 0.0.1

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