image:https://badge.fury.io/rb/mongo_odm.svg
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:
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:
api.mongodb.org/ruby/1.2.4/index.html
Credits¶ ↑
Carlos Paramio, carlosparamio.com.
See CONTRIBUTORS file for a list of contributions.
License¶ ↑
See LICENSE file for details.
Star History¶ ↑
image:https://api.star-history.com/svg?repos=carlosparamio/mongo_odm&type=Date