mongodb-mongo-activerecord-ruby 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +49 -0
- data/Rakefile +39 -0
- data/examples/tracks.rb +107 -0
- data/lib/mongo_record.rb +23 -0
- data/lib/mongo_record/base.rb +827 -0
- data/lib/mongo_record/convert.rb +45 -0
- data/lib/mongo_record/log_device.rb +113 -0
- data/lib/mongo_record/sql.rb +237 -0
- data/lib/mongo_record/subobject.rb +111 -0
- data/mongo-activerecord-ruby.gemspec +35 -0
- data/tests/address.rb +12 -0
- data/tests/course.rb +10 -0
- data/tests/student.rb +34 -0
- data/tests/test_log_device.rb +79 -0
- data/tests/test_mongo.rb +605 -0
- data/tests/test_sql.rb +176 -0
- data/tests/track2.rb +9 -0
- data/tests/track3.rb +9 -0
- metadata +79 -0
data/README.rdoc
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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
|
data/examples/tracks.rb
ADDED
@@ -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
|
data/lib/mongo_record.rb
ADDED
@@ -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
|