= mongo_odm
Flexible persistence module for any Ruby class to MongoDB.
= Why another ODM for MongoDB?
- Fully compatible with Rails 3
- Use the Mongo ruby driver when possible (query syntax, cursors, indexes management...)
- Allow lazy loading of collections and queries nesting (concatenation of 'find' calls) to emulate ActiveRecord 3
- No association methods (for now): Just declare your own methods on models to fetch the related items
- Give support for dirty objects, validations, etc. through ActiveModel 3
- Automanage type conversions and default values
- Keep it as simple as possible
= Basics
Other Mongo ODMs don't require to explicitly define the possible schema of a model. I think this is necessary to help with type conversions (instanciate the right class for each attribute, and convert them to a Mongo compatible type when persisted). But it's also possible to fill attributes with valid Mongo values without defining them as fields, and only the attributes whose values are different than the default values are stored as part of the document when saved.
A piece of code is better than a hundred of words:
Establish connection; it uses localhost:27017 and database 'test' if not specified
MongoODM.config = {:host => 'localhost', :port => 27017, :database => "my_tests"}
class Shape
include MongoODM::Document
field :name
field :x, Float, :default => 0.0
field :y, Float, :default => 0.0
end
shape = Shape.new(:name => "Point", :x => 0, :y => 5)
shape.save
Saves:
{ "_id" : ObjectId("4be97178715dd2c4be000006"),
"_class" : "Shape",
"x" : 0,
"y" : 5,
"color" : null,
"name" : "Point"
}
class Circle < Shape # This items are stored on the 'shapes' collection
field :radius, Float, :default => 1.0
end
circle = Circle.new.save
Saves:
{ "_id" : ObjectId("4be97203715dd2c4be000007"),
"_class" : "Circle",
"x" : 1,
"y" : 1,
"color" : null,
"radius" : 1 }
all_shapes = Shape.find # Returns a criteria object. It will execute the query and instance the objects once you iterate over it
all_shapes.to_a
Returns all the shapes; notice they are of different classes:
[ #<Shape x: 0.0, y: 5.0, color: nil, name: "Point", _id: {"$oid"=>"4be97178715dd2c4be000006"}>,
#<Circle x: 1.0, y: 1.0, color: nil, radius: 1.0, _id: {"$oid"=>"4be97293715dd2c4be000008"}> ]
In fact, you can instanciate any document stored as a hash to the appropiate class. The document just need
to have the attribute "_class" set to the name of the class you want to use as the object type. Example:
MongoODM.instanciate({ :x => 12, :y => 5, '_class' => 'Circle' })
Returns:
#<Circle x: 12.0, y: 5.0, color: nil, radius: 1.0>
And because any query method returns a MongoODM::Criteria object, you can concatenate them to nest several conditions (like if they were ActiveRecord scopes):
Shape.find(:radius => 1).find({}, {:sort => [:color, :asc]}) # Returns a criteria object. Once you iterate over it, it will run a query with both the :radius selector and :sort order.
You can also define your own class methods that returns criteria objects, and concatenate them to obtain a single criteria with all the conditions merged in the calls order:
class Shape
include MongoODM::Document
def self.with_radius(n)
find(:radius => n)
end
def self.ordered_by_color
find({}, {:sort => [:color, :asc]})
end
end
Shape.with_radius(1).ordered_by_color # Returns the same criteria than the previous example
Default values for fields can be either a fixed value, or a block, in which case the block will be called each time an object is instantiated. Example:
class Timestamp
include MongoODM::Document
field :value, Time, :default => lambda { Time.now }
field :set, Set, :default => lambda { Set.new }
end
Take a look at the Mongo Ruby driver documentation for the 'find' method to see the available options:
http://api.mongodb.org/ruby/1.2.4/Mongo/Collection.html#find-instance_method
= Collections
By default, mongo_odm stores data on a collection with the same name than the class, pluralized. In case of class inheritance, it uses the name of the parent class. You can override this behavior by setting a different collection for a class:
class Shape
set_collection 'my_shapes'
end
Alternatively, you can pass a MongoODM::Collection instance to set_collection, to indicate not only a collection name, but also a different connection and/or database:
class Shape
set_collection MongoODM::Collection.new(MongoODM.connection.db('another_database'), 'my_shapes')
end
= References
You can use BSON::DBRef as the type of a field. This acts as a pointer to any other document in your database, at any collection. If you assign a MongoODM::Document instance to a BSON::DBRef field, it will be converted to a reference automatically. To instantiate any reference object, just call "dereference" on it. To convert any MongoODM::Document object to a reference, just call "to_dbref" on it.
You can even dereference a full array or hash that contains BSON::DBRef instances! It will dereference them at any level.
class Node
include MongoODM::Document
field :name
field :parent, BSON::DBRef
field :children, Array
end
root_node = Node.new(:name => 'root')
root_node.save
children1 = Node.new(:name => 'children1', :parent => root_node)
children1.save
root_node.children = [children1.to_dbref]
root_node.save
children1.parent # Returns BSON::DBRef(namespace:"nodes", id: "4d60e8c83f5f19cf08000001")
root_node.children # Returns [BSON::DBRef(namespace:"nodes", id: "4d60e8c83f5f19cf08000002")]
children1.parent.dereference # Returns #<Node _id: BSON::ObjectId('4d60e8c83f5f19cf08000001'), children: [BSON::DBRef(namespace:"nodes", id: "4d60e8c83f5f19cf08000002")], name: "root", parent: nil>
root_node.children.dereference # Returns [#<Node _id: BSON::ObjectId('4d60e8c83f5f19cf08000002'), children: nil, name: "children1", parent: BSON::DBRef(namespace:"nodes", id: "4d60e8c83f5f19cf08000001")>]
= Associations
To embed just one copy of another class, just define the field type of that class. The class just need to respond to the "type_cast" class method and the "to_mongo" instance method. Example:
class RGB
def initialize(r, g, b)
@r, @g, @b = r, g, b
end
def inspect
"RGB(#{@r},#{@g},#{@b})"
end
def to_mongo
[@r, @g, @b]
end
def self.type_cast(value)
return nil if value.nil?
return value if value.is_a?(RGB)
return new(value[0], value[1], value[2]) if value.is_a?(Array)
end
end
class Color
include MongoODM::Document
field :name
field :rgb, RGB
index :name, :unique => true
end
Color.create_indexes # You can also use MongoODM.create_indexes to create all the indexes at all classes at the same time
color = Color.new(:name => "red", :rgb => RGB.new(255,0,0))
color.save
Saves:
{"_class":"Color","name":"red","rgb":[255,0,0],"_id":{"$oid": "4bf070fb715dd271c2000001"}}
red = Color.find({:name => "red"}).first
Returns:
#<Color name: "red", rgb: RGB(255,0,0), _id: {"$oid"=>"4bf070fb715dd271c2000001"}>
Of course, if the embedded object's class includes the MongoODM::Document module, you don't need to
define those methods. Just define the field as that class:
class RGB
include MongoODM::Document
field :r, Fixnum
field :g, Fixnum
field :b, Fixnum
end
class Color
include MongoODM::Document
field :name
field :rgb, RGB
end
color = Color.new(:name => "red", :rgb => RGB.new(:r => 255, :g => 0, :b => 0))
color.save
Saves:
{"_class":"Color","name":"red","rgb":{"_class":"RGB","r":255,"g":0,"b":0},"_id":{"$oid": "4bf073e3715dd27212000001"}}
red = Color.find({:name => "red"}).first
Returns:
#<Color name: "red", rgb: #<RGB r: 255, g: 0, b: 0>, _id: {"$oid"=>"4bf073e3715dd27212000001"}>
If you want to save a collection of objects, just define the field as an Array. You can even store objects of different types!
class Shape
include MongoODM::Document
field :x, Float
field :y, Float
end
class Circle < Shape
include MongoODM::Document
field :radius, Float
end
class Line < Shape
include MongoODM::Document
field :dx, Float
field :dy, Float
end
class Draw
include MongoODM::Document
field :objects, Array
end
circle1 = Circle.new(:x => 1, :y => 1, :radius => 10)
circle2 = Circle.new(:x => 2, :y => 2, :radius => 20)
line = Line.new(:x => 0, :y => 0, :dx => 10, :dy => 5)
draw = Draw.new(:objects => [circle1, line, circle2])
draw.save
Saves:
{ "_class" : "Draw",
"objects" : [ { "_class" : "Circle",
"x" : 1.0,
"y" : 1.0,
"color" : null,
"radius" : 10.0 },
{ "_class" : "Line",
"x" : 0.0,
"y" : 0.0,
"color" : null,
"dx" : 10.0,
"dy" : 5.0},
{ "_class" : "Circle",
"x" : 2.0,
"y" : 2.0,
"color" : null,
"radius" : 20.0 } ],
"_id":{"$oid": "4bf0775d715dd2725a000001"}}
Draw.find_one
Returns
#<Draw objects: [#<Circle x: 1.0, y: 1.0, color: nil, radius: 10.0>, #<Line x: 0.0, y: 0.0, color: nil, dx: 10.0, dy: 5.0>, #<Circle x: 2.0, y: 2.0, color: nil, radius: 20.0>], _id: {"$oid"=>"4bf0775d715dd2725a000001"}>
To reference the associated objects instead of embed them, you can use BSON::DBRef (to reference one), Array (to reference several), and others:
class Flag
include MongoODM::Document
field :colors_refs, Array, :default => []
def add_color(color)
colors_refs << color.to_dbref
end
def colors
colors_refs.dereference
end
end
class Color
include MongoODM::Document
field :name
end
color_red = Color.new(:name => "red")
color_red.save
color_green = Color.new(:name => "green")
color_green.save
flag = Flag.new
flag.add_color(color_red)
flag.add_color(color_green)
flag.save
Saves:
{ "_id" : ObjectId("4be96c15715dd2c4be000003"),
"_class" : "Flag",
"colors_refs" : [
{ "$ns" : "colors",
"$id" : {
"$oid" : "4d60ea4e3f5f19cf10000001"
}
},
{ "$ns" : "colors",
"$id" : {
"$oid" : "4d60ea4e3f5f19cf10000002"
}
}
]
}
flag.colors # Returns [#<Color _id: BSON::ObjectId('4d60ea4e3f5f19cf10000001'), name: "red">, #<Color _id: BSON::ObjectId('4d60ea4e3f5f19cf10000002'), name: "green">]
flag.colors
Returns a criteria object that wraps a cursor
flag.colors.to_a
Returns:
[#<Color name: "red", _id: {"$oid"=>"4be96bfe715dd2c4be000001"}>, #<Color name: "green", _id: {"$oid"=>"4be96c08715dd2c4be000002"}>]
Or you can build your custon methods. Example:
class Flag
include MongoODM::Document
field :colors_ids, Array
def colors
Color.find(:_id => {'$in' => colors_ids})
end
end
class Color
include MongoODM::Document
field :name
end
Color.new(:name => "red").save
Color.new(:name => "green").save
flag = Flag.new(:colors_ids => [ Color.find_one(:name => "red").id, Color.find_one(:name => "green").id ])
flag.save
Saves:
{ "_id" : ObjectId("4be96c15715dd2c4be000003"),
"_class" : "Flag",
"colors_ids" : [ ObjectId("4be96bfe715dd2c4be000001"), ObjectId("4be96c08715dd2c4be000002") ]
}
flag.colors
Returns a criteria object that wraps a cursor
flag.colors.to_a
Returns:
[#<Color name: "red", _id: {"$oid"=>"4be96bfe715dd2c4be000001"}>, #<Color name: "green", _id: {"$oid"=>"4be96c08715dd2c4be000002"}>]
= Callbacks
For now, the available callbacks are: after_initialize, before_save, after_save
Example:
class User
include MongoODM::Document
field :encrypted_password
attr_accessor :password
before_save :encrypt_password
def encrypt_password
return if self.password.blank?
self.encrypted_password = encrypt(password)
end
protected :encrypt_password
end
= Validations
All the validation methods defined in ActiveModel::Validations are included
Example:
class User
include MongoODM::Document
field :email
validates_presence_of :email
validates_uniqueness_of :email, :case_sensitive => false
validates_format_of :email, :with => /^([a-zA-Z0-9_\.\-\+])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/
end
= Dirty
All the dirty object methods defined in ActiveModel::Dirty are included
Example:
class User
include MongoODM::Document
field :email
end
user = User.new
user.email = "hello@h1labs.com"
user.email_changed? # Returns true
user.email_change # Returns [nil, "hello@h1labs.com"]
user.changes # Returns {"email" => [nil, "hello@h1labs.com"]}
= Others
Access to a cursor to the whole collection:
User.cursor
Use cursor methods directly on the class:
User.has_next?
User.each{...}
User.next_document
User.rewind!
...
= TODO
- Allow to specify different database connections with each document definition
- Increase rspec coverage
- Document, document, document!
- Create useful modules to make common operations easier (versioning, trees, etc)
= More
For now, take a look at the Mongo Ruby driver syntax:
http://api.mongodb.org/ruby/1.2.4/index.html
= Credits
Carlos Paramio, http://h1labs.com.
See CONTRIBUTORS file for a list of contributions.
= License
See LICENSE file for details.