Git Product home page Git Product logo

mongo_odm's Introduction

<img src=“https://badge.fury.io/rb/mongo_odm.svg” alt=“Gem Version” />

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 = "[email protected]"
user.email_changed? # Returns true
user.email_change # Returns [nil, "[email protected]"]
user.changes # Returns {"email" => [nil, "[email protected]"]}

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, h1labs.com.

See CONTRIBUTORS file for a list of contributions.

License

See LICENSE file for details.

mongo_odm's People

Contributors

carlosparamio avatar codemonkeysteve avatar dusty avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

mongo_odm's Issues

Connection options get reset to default, when setting Mongo.config after Mongo class models are required

When a global ::MongoODM.config = {...} (e.g. outside of Rails) has been initialized, but after all the model classes are required, the connections always revert back to default settings, meaning the running code will use the default 'test' database. This results in hard to find bugs, as MongoODM.config still shows the correct database as set by the user, but the connection itself actually reverted back to 'test'. Fixed the issue by setting ::MongoODM.config = {...} before any model classes are required.

Config settings in Initializer?

This may be a feature request or a support request, not sure yet, but is there a way to connect to Mongo via an initializer? I don't want to have to declare config in multiple places and I'd rather not need to create custom config settings.

Is it possible to do this? Pull Request or STFU?

MongoODM::Criteria delegates count to Collection instead of Cursor

It seems to me that MongoODM::Criteria delegates count method to Collection instead of Cursor in method_missing. The following code

User.find({ :active => true }).count

should return number of active users but returns count of documents in the collection instead.

Support for ActiveSupport::TimeWithZone objects

When tries to assign an ActiveSupport::TimeWithZone to a field, it complains with:

BSON::InvalidDocument: ActiveSupport::TimeWithZone is not currently supported; use a UTC Time instance instead.

Do the conversion automatically when the document is persisted.

bson compatibility issues

There seems to be a version incompatibility between mongo 1.3.1 and bson 1.7.0. Here's the stack trace I got while running one of the examples.

#!/usr/bin/ruby
require 'rubygems'
require 'mongo_odm'
MongoODM.config = {:host => 'localhost', :port => 27017, :database => "test"}

class Shape
  include MongoODM::Document
  field :name
  field :x, Float, :default => 0.0
  field :y, Float, :default => 0.0
end

s = Shape.new(:name=>"Point", :x=>0, :y=>0)
s.save

Stack trace:

/Users/james/.rvm/gems/ruby-1.9.3-p194/gems/bson-1.7.0/lib/bson/bson_c.rb:24:in `serialize': wrong number of arguments(4 for 3) (ArgumentError)
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/bson-1.7.0/lib/bson/bson_c.rb:24:in `serialize'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo-1.3.1/lib/mongo/cursor.rb:425:in `construct_query_message'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo-1.3.1/lib/mongo/cursor.rb:405:in `send_initial_query'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo-1.3.1/lib/mongo/cursor.rb:371:in `refresh'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo-1.3.1/lib/mongo/cursor.rb:87:in `next_document'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo-1.3.1/lib/mongo/db.rb:497:in `command'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo-1.3.1/lib/mongo/connection.rb:704:in `check_is_master'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo-1.3.1/lib/mongo/connection.rb:504:in `connect'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo-1.3.1/lib/mongo/connection.rb:656:in `setup'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo-1.3.1/lib/mongo/connection.rb:101:in `initialize'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo_odm-0.2.20/lib/mongo_odm/config.rb:39:in `new'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo_odm-0.2.20/lib/mongo_odm/config.rb:39:in `connection'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo_odm-0.2.20/lib/mongo_odm.rb:22:in `connection'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo_odm-0.2.20/lib/mongo_odm.rb:30:in `database'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo_odm-0.2.20/lib/mongo_odm/document/persistence.rb:76:in `collection'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo_odm-0.2.20/lib/mongo_odm/document/persistence.rb:67:in `save'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo_odm-0.2.20/lib/mongo_odm/document/persistence.rb:38:in `block in save'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/activesupport-3.0.17/lib/active_support/callbacks.rb:414:in `_run_save_callbacks'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo_odm-0.2.20/lib/mongo_odm/document/persistence.rb:37:in `save'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo_odm-0.2.20/lib/mongo_odm/document/attribute_methods/dirty.rb:33:in `save_with_dirty'
    from /Users/james/.rvm/gems/ruby-1.9.3-p194/gems/mongo_odm-0.2.20/lib/mongo_odm/document/validations.rb:31:in `save_with_validation'
    from mongo_odm_test.rb:14:in `<main>'

Document fields initialization improvement proposal (?)

I have found that MongoODM::Document mixes default field values with values given to new method as arguments during fields initialization. I found situation when this behavior doesn't play well. The typical case is when document contains faked "field" which just accepts/returns another representation of "master" field. Let's have following sample document:

class Post
    include MongoODM::Document
    field :tags_array, Array, :default => []
    # other fields ...

    def tags
        self.tags_array.join(',')
    end

    def tags=(value)
        self.tags_array = value.split(',').map(&:strip).reject(&:blank?)
    end
end

When Post instance is edited via web form, tags can be used instead of tags_array and user can easily edit tags as comma separated list in a text field. tags_array is than usable everywhere tags should be processed as array one by one.

Now, let's have following code which initializes new Post:

post = Post.new(:tags => 'ruby,mongo_odm')

User (read "me" :) now expects that newly created post will have tags_array set to [ "ruby", "mongo_odm" ] but the truth is that tags_array will be empty. It's because both default and given values are processed at the same time and it depends on internal fields ordering in a attributes hash if tags will be set first and tags_array will than overwrite it with the default value or vice versa.

I'm not really sure, if MongoODM should take into account such special behavior as I'm pretty new to Ruby and Rails and maybe there are some prettier solutions for mentioned situation. I found the problem when I tried to port mongoid_taggable gem (easy and transparent tagging support for any document) from Mongoid to MongoODM.

What do you think about it?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.