mongodb-mongo-activerecord-ruby 0.0.1
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/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
|