Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Flexible persistence module for any Ruby class to MongoDB

Fetching latest commit…

Cannot retrieve the latest commit at this time

README.rdoc

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

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.

Basics

A piece of code is better than a hundred of words:

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
  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

Take a look at the Mongo Ruby driver documentation for the 'find' method to see the available options:

api.mongodb.org/ruby/1.0.8/Mongo/Collection.html#find-instance_method

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

  create_index :name, :unique => true
end

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, for now you need to define your own 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
  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
  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"]}

TODO

* Support join of different concatenated find calls to a single criteria object
* Add helpers to define attributes as referenced objects
* 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, localization, etc)

More

For now, take a look at the Mongo Ruby driver syntax:

api.mongodb.org/ruby/1.0.8/index.html

Copyright

Copyright © 2010 Carlos Paramio. See LICENSE for details.

Credits

Carlos Paramio

h1labs.com

Something went wrong with that request. Please try again.