Mongocore Ruby Database Driver

A new MongoDB ORM implementation on top of the latest MongoDB Ruby driver. Very fast and light weight.

The perfect companion for Rails, Sinatra, Susana or other Rack-based web frameworks.

Features

With Mongocore you can do:

  • Insert, update and delete
  • Finding, sorting, limit, skip, defaults
  • Scopes, associations, validations, pagination
  • Read and write access control for each key
  • Request cache, counter cache, track changes
  • Automatic timestamps, tagged keys, json

The schema is specified with a YAML file which supports default values, data types, and security levels for each key.

Please read the source code to see how it works, it's fully commented and very small, only 8 files, and 519 lines of fully test driven code.

Library Files Comment Lines of code
Mongoid 256 14371 10590
MongoMapper 91 200 4070
Mongocore 8 275 519


The tests are written using Futest, try it out if you haven't, it makes testing so much fun.

Installation

gem install mongocore

or add to your Gemfile.

Then in your model:

class Model
  include Mongocore::Document
end

Settings

Mongocore has a few built in settings you can easily toggle:

# Schema path is $app_root/config/db/schema/:model.yml
# The yml files should have singular names
Mongocore.schema = File.join(Dir.pwd, 'config', 'db', 'schema')

# The cache stores documents in memory to avoid db round trips
Mongocore.cache = true

# The access enables the read / write access levels for the keys
Mongocore.access = true

# Enable timestamps, auto-save created_at and updated_at keys
Mongocore.timestamps = true

# Default sorting, last will be opposite. Should be indexed.
Mongocore.sort = {}

# Pagination results per page
Mogocore.per_page = 20

# Enable debug to see caching information and help
Mongocore.debug = false

Usage

# Set up connection to database engine
# Add this code to an initializer or in your environment file
Mongocore.db = Mongo::Client.new(['127.0.0.1:27017'], :database => "dbname_#{ENV['RACK_ENV']}")

# Logging options
Mongo::Logger.logger.level = ::Logger::INFO
Mongo::Logger.logger.level = ::Logger::FATAL

# Write to log file instead of terminal
Mongo::Logger.logger = ::Logger.new('./log/mongo.log')

# Create a new document
m = Model.new
m.duration = 59
m.save

# Insert and save in one line
m = Model.insert(:duration => 45, :goal => 55)
m = Model.create(params, :validate => true) # Alias

# Create another document
p = Parent.new(:house => 'Nice')
p.save

# Reload the model attributes from the database
p.reload

# Add the parent to the model
m.parent = p
m.save

# Finding
query = Model.find
query = Model.where # Alias

# Query doesn't get executed until you call all, count, last or first
m = query.all
a = query.featured.all
c = query.count
l = query.last
f = query.first

# All
m = Model.find.all

# Pagination returns an array
m = Model.find.paginate
m = Model.find.paginate(:per_page => 10, :page => 5)
m.total # => Total number of results

# Use each to fetch one by one
Model.each do |m|
  puts m
end

# each_with_index, each_with_object and map works as well

# Works with finds, scopes and associations
Model.find(:duration => 50).each{|m| puts m}

# All of these can be used:
# https://docs.mongodb.com/manual/reference/operator/query-comparison
m = Model.find(:house => {:$ne => nil, :$eq => 'Nice'}).last

# Sorting, use -1 for descending, 1 for ascending
m = Model.find({:duration => {:$gt => 40}}, :sort => {:duration => -1}).all
m = p.models.find(:duration => 10).sort(:duration => -1).first

# Limit, pass as third option to find or chain, up to you
p = Parent.find.sort(:duration => 1).limit(5).all
p = Parent.limit(1).last
m = p.models.find({}, :sort => {:goal => 1}, :limit => 1).first
m = Model.sort(:goal => 1, :duration => -1).limit(10).all

# First
m = Model.find(:_id => object_id).first
m = Model.find(object_id).first
m = Model.find(string).first
m = Model.find(:duration => 60, :goal => {:$gt => 0}).first

# Last
m = Model.last
m = p.models.last

# Count
c = Model.count
c = p.models.featured.count

# Skip
m = Model.find.skip(2).first

# Attributes
m = Model.first
m.attributes             # => All attributes
m.attributes(:badge)     # => Attributes with the badge tag only
m.to_json                # => All attributes as json

# Track changes
m.duration = 33
m.changed?
m.duration_changed?
m.duration_was
m.changes
m.saved?
m.persisted? # Alias for saved?
m.unsaved?
m.new_record? # Alias for unsaved?

# Validate
m.valid?
m.errors.any?
m.errors

# Update
m.update(:duration => 60)

# Delete
m.delete

# Many associations
q = p.models.all
m = p.models.first
m = p.models.last

# Scopes
q = p.models.featured.all
q = p.models.featured.nested.all
m = Model.featured.first

# Access
model = Mongocore::Access.role(:user) do
  # Reads and writes in the block will be with the above access level
  Model.first
end

# In your model
class Model
  include Mongocore::Document

  # Validations will be run if you pass model.save(:validate => true)
  # You can run them manually by calling model.valid?
  # You can have multiple validate blocks if you want to
  validate do
    # The errors hash can be used to collect error messages.
    errors[:duration] << 'duration must be greater than 0' if duration and duration < 1
    errors[:goal] << 'you need a higher goal' if goal and goal < 5
  end

  # Before and after filters: :save, :delete
  # You can have multiple blocks for each filter if needed
  before :save, :setup

  def setup
    puts "Before save"
  end

  after :delete do
    puts "After delete"
  end
end

# Use pure Ruby driver, returns BSON::Document objects
Mongocore.db[:models].find.to_a
Mongocore.db[:models].find({:_id => m._id}).first

# Indexing
Mongocore.db[:models].indexes.create_one(:key => 1)
Mongocore.db[:models].indexes.create_one({:key => 1}, :unique => true)

Schema and models

Each model is defined using a YAML schema file. This is where you define keys, defaults, description, counters, associations, access, tags, scopes and accessors.

The default schema file location is APP_ROOT/config/db/schema/*.yml, so if you have a model called Parent, create a yml file called parent.yml.

You can change the shema file location like this:

Mongocore.schema = File.join(Dir.pwd, 'your', 'schema', 'path')

Parent example schema, has many Models


# The meta is information about your model
meta:
  name: parent
  type: document

keys:

  # Define the _id field for all your models. The id field (without _)
  # is an alias to _id, but always returns a string instead of a BSON::ObjectId
  # Any object ids as strings will be automatically converted into ObjectIds
  # @desc: Describes the key, can be used for documentation.
  # @type: object_id, string, integer, float, boolean, time, binary, hash, array
  # @default: the default value for the key when you call .new
  # @read: access level for read: all, user, owner, dev, admin, super, app
  # @write: access level for write. Returns nil if no access, as on read

  # Object ID, usually added for each model
  _id:
    desc: Unique id
    type: object_id
    read: all
    write: app

  # String key
  world:
    desc: Parent world
    type: string
    read: all
    write: user

  # If the key ends with _count, it will be used automatically when
  # you call .count on the model as an automatic caching mechanism
  models_count:
    desc: Models count
    type: integer
    default: 0
    read: all
    write: app

  # This field will be returned when you write models.featured.count
  # Remember to create an after filter to keep it updated
  models_featured_count:
    desc: Models featured count
    type: integer
    default: 0
    read: all
    write: app

# Many relationships lets you do:
# Model.parents.all or model.parents.featured.all with scopes
many:
  - models

Model example schema, belongs to Parent

meta:
  name: model
  type: document

keys:
  # Object ID
  _id:
    desc: Unique id
    type: object_id
    read: all
    write: app

  # Integer key with default
  duration:
    desc: Model duration in days
    type: integer
    default: 60
    read: dev
    write: user
    # Add tags for keys for use with attributes
    tags:
    - badge

  # Time key
  expires_at:
    desc: Model expiry date
    type: time
    read: all
    write: dev
    # Multiple tags possible: attributes(:badge, :campaigns)
    tags:
    - badge
    - campaigns

  # Hash key
  location_data:
    desc: Model location data
    type: hash
    read: all
    write: user

  # Counter key
  votes_count:
    desc: Votes count
    type: integer
    default: 0
    read: all
    write: dev
    tags:
    - badge

  # If the key ends with _id, it is treated as a foreign key,
  # and you can access it from the referencing model and set it too.
  # Example: model.parent, model.parent = parent
  parent_id:
    desc: Parent id
    type: object_id
    read: all
    write: dev

# Generate accessors (attr_accessor) for each key
accessor:
- submittable
- update_expires_at
- skip_before_save

# Define scopes that lets you do Models.featured.count
# Each scope has a name, and a set of triggers
scopes:

  # This will create a .featured scope, and add :duration => 60 to the query.
  featured:
    duration: 60

  nested:
    goal: 10

  # Any mongodb driver query is possible
  finished:
    duration: 60
    goal:
      $gt: 10

  active:
    params:
      - duration
    duration:
      $ne: duration

  # You can also pass parameters into the scope, as a lambda.
  ending:
    params:
     - user
    $or:
      - user_id: user.id
      - listener: user.id
      - listener: user.link
    deletors:
      $ne: user.id

Contribute

Contributions and feedback are welcome! MIT Licensed.

Issues will be fixed, this library is actively maintained by Fugroup Ltd. We are the creators of CrowdfundHQ.

Thanks!

@authors: Vidar