The purpose of this brief guide is to get you familiar enough with MongoFrames to decide if you want to use it on your next project. If you do decide to then there's lots of additional documentation and examples in the API docs and Snippets page - if by the end you're screaming "WHAT THE FOOBAR" at the screen, well, at least you didn't waste much time on it.

Prerequisites

You'll need access to a MongoDB database and to run your code using Python 3.4+.

It's probably safe to use Python 3+ but we don't run tests for versions below 3.4.

Installing

MongoFrames is on the Python Package Index (PyPI), so it can be installed using pip:

$ pip install MongoFrames

Connecting to the database

MongoFrames builds on the excellent PyMongo library for working with MongoDB, the simplest way to connect to your database is using the MongoClient class provided by PyMongo like so:

from mongoframes import *
from pymongo import MongoClient

# Connect MongoFrames to the database
Frame._client = MongoClient('mongodb://localhost:27017/mydb')

Framing documents

Frame and SubFrame are base classes for mapping MongoDB documents and embedded documents to Python. Defining a Frame or SubFrame is straightforward:

class Dragon(Frame):

    _fields = {
        'name',
        'breed',
        'stashed_items'
        }

class Item(SubFrame):

    _fields = {
        'desc',
        'worth'
        }

By default the collection name will be the same as the class name (the collection doesn't need to exist on the database beforehand). If you want to override this behaviour you can specify the collection directly using the _collection class attribute.

Inserting, updating and deleting

The Frame class provides a set of methods for inserting, updating and deleting documents either individually or in multiples:

# Create and insert a dragon
burt = Dragon(
    name='Burt',
    breed='Fire-drake',
    stashed_items=[
        Item(desc='Rotting lamb carcass', worth=1),
        Item(desc='Montecristo No. 2', worth=30)
    ])
burt.insert()

# Create and insert multiple dragons in a single operation
others = Dragon.insert_many([
    {
        'name': 'Edison',
        'breed': 'Ice-drake',
        'stashed_items': [Item(desc='Rubber chicken', worth=0), ...]
    },
    ...
    ])

# Update a dragon
burt.breed = 'Lesser fire-drake'
burt.stashed_items.append(Item(desc="A hobbit's foot", worth=5))
burt.update()

# Delete a dragon
burt.delete()

There are update_many and delete_many class methods also.

Finding and querying

The Frame class provides methods for retrieving documents from the database and MongoFrames offers a concise way to build queries using the Q class (see the Query API):

Using the Q class and associated helpers to build queries is entirely optional, if you prefer you can pass queries in the more native format that PyMongo accepts.

# Retrieve one dragon (Burt) from the database
burt = Dragon.one(Q.name == 'Burt')

# Retrieve the first ten Ice-drakes except Edison ordered by name
fire_drakes = Dragon.many(
    And(Q.breed == 'Ice-drake', Q.name != 'Edison'),
    limit=10,
    sort=[('name', ASC)]
    )

You can also paginate results using the Paginator class (see the Pagination API):

# Paginate a list of all dragons
pages = Paginator(Dragon, sort=[('name', ASC)])

# Print the name of each dragon on the first page
for dragon in pages[1]:
    print(dragon.name)

Projections

To determine which fields are returned for documents you retrieve MongoDB allows you to specify a projection, just like MongoDB you can send projections to query methods in MongoFrames:

# Find all dragons selecting only the name field
dragons = Dragon.many(projection={'name': True})

But projections play a bigger role in MongoFrames, mapping SubFrames to embedded documents and providing support for dereferencing.

SubFrames

When selecting a document that contains embedded documents you may want to map them to a SubFrame class, this is achieved through projections and the special $sub key:

# Select all dragons, all fields, and map the `stashed_items` field to an `Item`
# instance.
dragons = Dragon.many(projection={'stashed_items': {'$sub': Item}})

If the majority of the time you want to use this projection when selecting documents for this collection you can set it as the default:

# Set the default projection
Dragon._default_projection = {'stashed_items': {'$sub': Item}}​

Dereferencing

Projections also allow reference fields (typically fields containing an ObjectId) to be dereferenced (mapped to a document).

To be clear when I talk about reference fields in MongoFrames' projections I am describing a scenario where a field's value consists of one or more reference values (typically ObjectIds) that reference other documents in the same database by their _id field. It is of course perfectly possible in MongoDB for an ObjectId to not be a reference to another document. This is a construct of MongoFrames and not MongoDB.

Let's imagine that each dragon has a lair field that stores the ID of a document in the Lair collection:

# Define our new and improved Dragon class and along with Lair
class Dragon(Frame):

    _fields = {
        'name',
        'breed',
        'stashed_items',
        'lair'
        }

class Lair(Frame):

    _fields = {
        'name',
        'lat_lng'
        }

# Insert a lair in the database
mount_doom = Lair(name='Mount Doom', lat_lng=[-39.156833, 175.632167])
mount_doom.insert()

# Make Mount Doom Burt's new lair
burt.lair = mount_doom
burt.update()

If we retrieve Burt from the database without mapping the lair field to a reference class the field will contain an ObjectId:

burt = Dragon.one(Q.name == 'Burt')
print(burt.lair)

>> ObjectId('...')

To convert the ObjectId to a Lair instance (e.g dereference the field) we use a projection and the special $ref key:

burt = Dragon.one(Q.name == 'Burt', projection={'lair': {'$ref': Lair}})
print(burt.lair.name)

>> 'Mount Doom'

If you have questions about performance or are just interested, the mechanism used to dereference documents is described in the Projections and Performance sections of Frames and SubFrames API.

Listening for events

Any of the Frame's insert, update, and delete methods will trigger events, one before the action is taken and one after, for example the events triggered when you call insert or insert_many are insert and inserted.

You can listen and react to events by binding a function to a named event using the listen method:

# Nullify the `lair` field whenever a referenced lair is deleted
def on_delete(sender, frames):
    Lair.nullify(Dragon, 'lair', frames)

Lair.listen('deleted', on_delete)

There's a more detailed description of events (and the integrity methods available; nullify, cascade and pull) in the Events section of Frames and SubFrames API.

Fin

That's all for our brief tour of MongoFrames, if I've done enough to convince you it's worth trying for your next project then I recommend you review the API and Snippet sections which provide more extensive documentation and detailed usage examples.

But what about validation, indexes and space boots?

By design MongoFrames provides a concise set of features, you can dissect and understand the entire code base in an hour or so, magic is kept to a minimum and consequently you'll feel comfortable modifying and extending the library to meet your needs from the get go.

To that end we've collected together a number of snippets that provide behaviour we think is neat or that we know some users expect from their ODM (like data validation) - even if we don't :)

So checkout the snippets section for data validation, index management and other neat suff.

* There's no space boots :(