Ming Foundation Layer¶
MongoDB and Ming¶
MongoDB is a high-performance schemaless database that allows you to store and retrieve JSON-like documents. MongoDB stores these documents in collections, which are analogous to SQL tables. Because MongoDB is schemaless, there are no guarantees given to the database client of the format of the data that may be returned from a query; you can put any kind of document into a collection that you want.
While this dynamic behavior is handy in a rapid development environment where you might delete and re-create the database many times a day, it starts to be a problem when you need to make guarantees of the type of data in a collection (because your code depends on it). The goal of Ming is to allow you to specify the schema for your data in Python code and then develop in confidence, knowing the format of data you get from a query.
As Ming is heavily inspired by SQLAlchemy its Object Document Mapper layer
is mapped over a foundation layer (through the Mapper
class) that
provides the basic validation and connection management features over dictionaries.
Connecting to the Database¶
Ming manages your connection to the MongoDB database using an object known as a
DataStore
. The DataStore is actually just a thin wrapper around a pymongo
Database object which is used by ming.Session
to perform the actual
queries:
from ming import Session
session = Session(create_datastore('mongodb://localhost:27017/tutorial'))
Note
Note that Session
is thread-safe by itself as it stores no data,
differently from the base ODMSession
class which requires to be
used through ThreadLocalODMSession
to be thread safe.
Collection objects¶
Now that that boilerplate is out of the way, we can actually start writing our models. We will start with a model representing a WikiPage. We can do that in “imperative” mode as follows:
from ming import collection, Field, schema
WikiPage = collection('wiki_page', session,
Field('_id', schema.ObjectId),
Field('title', schema.String),
Field('text', schema.String)
)
Here, we define a WikiPage Python class with three fields, _id, title, and text. We also bind the WikiPage to the session we defined earlier and to the wiki_page name.
If you prefer a “declarative” style, you can also declare a collection by subclassing
the Document
class:
from ming import Field, schema
from ming.declarative import Document
class WikiPage(Document):
class __mongometa__:
session = session
name = 'wiki_page'
_id = Field(schema.ObjectId)
title = Field(schema.String)
text = Field(schema.String)
Here, rather than use the collection() function, we are defining the class directly, grouping some of the metadata used by ming into a __mongometa__ class in order to reduce namespace conflicts. Note that we don’t have to provide the name of our various Field instances as strings here since they already have names implied by their names as class attributes. If we want to map a document field to a different class attribute, we can do so using the following syntax:
_renamed_field = Field('renamed_field', schema.String)
This is sometimes useful for “privatizing” document members that we wish to wrap in @property decorators or other access controls.
We can add our own methods to the WikiPage class, too. However, the make() method is reserved for object construction and validation. See the Bad Data section.
Type Annotations¶
Some type annotations are in Ming, but you need to add a hint to each class to help. You must be using the “declarative” approach that inherits from Document. The primary goal so far is to improve IDE experience. They may or may not work with mypy. Add some imports and the m: line to your models like this:
import typing
if typing.TYPE_CHECKING:
from ming.metadata import Manager
...
class WikiPage(Document):
class __mongometa__:
session = session
name = 'wiki_page'
m: 'Manager[WikiPage]'
...
Using Ming Objects to Represent Mongo Records¶
Now that we’ve defined a basic schema, let’s start playing around with Ming in the interactive interpreter. First, make sure you’ve saved the code below in a module “tutorial.py”:
from ming import Session, create_datastore
from ming import Document, Field, schema
bind = create_datastore('tutorial')
session = Session(bind)
class WikiPage(Document):
class __mongometa__:
session = session
name = 'wiki_page'
_id = Field(schema.ObjectId)
title = Field(str)
text = Field(str)
Now let’s fire up the interpreter and start working. The first thing we’ll do is create a WikiPage:
>>> page = WikiPage(dict(title='MyPage', text=''))
>>> page
{'text': '', 'title': 'MyPage'}
>>> page.title
'MyPage'
>>> page['title']
'MyPage'
As you can see, Ming documents
can be accessed
either using dictionary-style lookups (page[‘title’]) or attribute-style
lookups (page.title).
In fact, all Ming documents are dict
subclasses, so all the standard
methods on Python ict
objects are available.
In order to actually interact with the database, Ming provides a standard
attribute .m
, short for Manager, on each mapped class.
In order to save the document we just created to the database, for instance, we would simply type:
>>> page.m.save()
>>> page
{'text': '', '_id': ObjectId('4b1d638ceb033028a0000000'), 'title': 'MyPage'}
When the page was saved to the database, the database assigned a unique _id attribute. (If we had wished to specify our own _id, we could have also done that.) Now, let’s query the database and make sure that the document actually got saved:
>>> WikiPage.m.find().first()
{'text': u'', '_id': ObjectId('4b1d638ceb033028a0000000'), 'title': u'MyPage'}
And there it is! Now, let’s add some text to the page:
>>> page.text = 'This is some text on my page'
>>> page.m.save()
>>> WikiPage.m.find().first()
{'text': u'This is some text on my page', '_id': ObjectId('4b1d638ceb033028a0000000'), 'title': u'MyPage'}
Looks like it worked. One thing we glossed over was the use of the .m.find()
method. This is the main method we’ll use to query the database, and is covered
in the next section.
Querying the Database¶
Ming provides an .m
attribute that exposes the same methods available on
Session
just bound to the Collection or instance of your Documents.
The .m.find()
method works just like the .find()
method on collection
objects in pymongo and is used for performing queries.
The result of a query is a Python iterator that wraps a pymongo cursor,
converting each result to a ming.Document
before
yielding it.
Like SQLAlchemy, we provide several convenience methods on query results
through Cursor
:
- one()
Retrieve a single result from a query. Raises an exception if the query contains either zero or more than one result.
- first()
Retrieve the first result from a query. If there are no results, return
None
.- all()
Retrieve all results from a query, storing them in a Python
list
.- count()
Returns the number of results in a query
- limit(limit)
Restricts the cursor to only return limit results
- skip(skip)
Skips ahead skip results in the cursor (similar to a SQL OFFSET clause)
- sort(*args, **kwargs)
Sorts the underlying pymongo cursor using the same semantics as the
pymongo.Cursor.sort()
method
Ming also provides a convenience method .m.get(**kwargs)
which is equivalent to
.m.find(kwargs).first()
for simple queries that are expected to return one
result.
Some examples:
>>> WikiPage.m.find({'title': 'MyPage'}).first()
{'text': u'', '_id': ObjectId('4b1d638ceb033028a0000000'), 'title': u'MyPage'}
>>> WikiPage.m.find().count()
1
>>> WikiPage.m.get(title='MyPage')
{'text': u'', '_id': ObjectId('4b1d638ceb033028a0000000'), 'title': u'MyPage'}
Other Sessions¶
If we have a special case where we want to use a different database session for a model,
other than the one specified in __mongometa__
, we can do:
foobar = Session.by_name('foobar')
foobar.save(my_model_instance)
or:
foobar = Session.by_name('foobar')
my_model_instance.m(foobar).save()
This could be useful if you have a database session that is connected to a master server, and another one that is used for the slave (readonly).
Bad Data¶
So what about the schema? So far, we haven’t seen any evidence that Ming is doing anything with the schema information at all. Well, the first way that Ming helps us is by making sure we don’t specify values for properties that are not defined in the object:
>>> page = tutorial.WikiPage(dict(title='MyPage', text='', fooBar=''))
>>> page
{'fooBar': '', 'text': '', 'title': 'MyPage'}
>>> page.m.save()
Traceback (most recent call last):
...
formencode.api.Invalid: <class 'tutorial.WikiPage'>:
Extra keys: set(['fooBar'])
OK, that’s nice and all, but wouldn’t it be nicer if we could be warned at
creation time?
Ming provides a convenice method make()
on the
ming.Document
with just such behavior:
>>> page = tutorial.WikiPage.make(dict(title='MyPage', text='', fooBar=''))
Traceback (most recent call last):
...
formencode.api.Invalid: <class 'tutorial.WikiPage'>:
Extra keys: set(['fooBar'])
We can also provide default values for properties via the if_missing parameter
on a Field
.
Change the definition of the text property in tutorial.py to read:
text = Field(str, if_missing='')
Now if we restart the interpreter (or reload the tutorial module), we can do the following:
>>> page = tutorial.WikiPage.make(dict(title='MyPage'))
>>> page
{'text': '', 'title': 'MyPage'}
Ming also supports supplying a callable as an if_missing value so you could put the creation date in a WikiPage like this:
from datetime import datetime
...
creation_date = Field(datetime, if_missing=datetime.utcnow)