1. Tutorial
This tutorial introduces MongoEngine by means of an example application — a simple tumblelog that can record blog posts of various types (text, image and link posts) along with comments. It is loosely based on the Tumblelog application from the Flask tutorial, but adapted to use MongoEngine rather than a relational database. For more details about how to use MongoEngine, see the User Guide.
The source code for this tutorial is available on GitHub.
1.1. Getting started
Before we can start, make sure that a copy of MongoDB is running in your environment. To install MongoEngine, simply run:
$ python -m pip install -U mongoengine
Before we can start using MongoEngine, we need to tell it how to connect to our instance of mongod. For this we use the connect() function. If running locally, the only argument we need to provide is the name of the MongoDB database to use:
from mongoengine import *
connect('tumblelog')
There are lots of options for connecting to MongoDB — for more information about them see the Connecting to MongoDB guide.
1.2. Defining our documents
MongoDB is schemaless, which means that no schema is enforced by the database — we may add and remove fields however we want and MongoDB won't complain. This makes life a lot easier in many regards, especially when there is a change to the data model. However, defining schemas for our documents can help to iron out bugs involving incorrect types or missing fields, and also allow us to define utility methods on our documents in the same way that traditional ORMs do.
In our Tumblelog application we need to store several different types of information. We will need to keep information about the author of each post, and we'll also need to store different types of posts (text, image and link). The schema for this is very straight-forward, and translates directly into MongoEngine documents — but first let's think about our data.
1.2.1. Users
The key difference is that this schema will never be passed on to MongoDB — this will only be enforced at the application level, making future changes easy to manage. Also, the User documents will be stored in a MongoDB collection rather than a table.
class User(Document):
email = StringField(required=True)
first_name = StringField(max_length=50)
last_name = StringField(max_length=50)
This looks similar to how the structure of a form or ORM model might be defined. We provide some information about each field by assigning a field object as a class attribute. Fields may also take keyword arguments, allowing some validation to be attached to a specific field. There are many more field types available (e.g. DateTimeField, ReferenceField, etc.) — for a full list, see the API Reference.
1.2.2. Posts, Comments and Tags
Now we'll think about how to store the rest of the information. If we were using a relational database, we would most likely have a table of posts, a table of comments and a table of tags. To associate the comments with individual posts, we would put a column in the comments table that contained a foreign key to the posts table. We'd also need a link table to provide the many-to-many relationship between posts and tags. Then we'd need to address the problem of storing the specialised post-types (text, image and link). There are several ways we can achieve this, but each of them have their problems — none of them stand out as particularly intuitive solutions.
We will store all of the posts in one collection and each post type will only store the fields it needs. This fits with the Object-Oriented principle of inheritance nicely. In fact, MongoEngine supports this kind of modeling out of the box — all you need do is turn on inheritance by setting allow_inheritance to True in the meta:
class Post(Document):
title = StringField(max_length=120, required=True)
author = ReferenceField(User)
meta = {'allow_inheritance': True}
class TextPost(Post):
content = StringField()
class ImagePost(Post):
image_path = StringField()
class LinkPost(Post):
link_url = StringField()
We are storing a reference to the author of the posts using a ReferenceField object. These are similar to foreign key fields in traditional ORMs, and are automatically translated into references when they are saved, and dereferenced when they are loaded from the database.
1.2.3. Comments
A comment is not a standalone document — it exists as part of a post. For this reason we define it as an EmbeddedDocument rather than a Document. Using MongoDB we can store the comments as a list of embedded documents directly on a post document. Using MongoEngine, we can define the structure of embedded documents, along with utility methods, in exactly the same way we do with regular documents:
class Comment(EmbeddedDocument):
content = StringField()
name = StringField(max_length=120)
We can then store a list of comment documents in our post document:
class Post(Document):
title = StringField(max_length=120, required=True)
author = ReferenceField(User)
tags = ListField(StringField(max_length=30))
comments = ListField(EmbeddedDocumentField(Comment))
1.2.4. Handling deletions of references
The ReferenceField object takes a keyword reverse_delete_rule for handling deletion rules if the reference is deleted. To delete all the posts if a user is deleted set the rule:
class Post(Document):
title = StringField(max_length=120, required=True)
author = ReferenceField(User, reverse_delete_rule=CASCADE)
tags = ListField(StringField(max_length=30))
comments = ListField(EmbeddedDocumentField(Comment))
See ReferenceField for more information about deletion rules.
1.3. Adding data to our Tumblelog
Now that we have defined our documents, let's start adding some data. Firstly, we'll need to create a User object. We can do this by creating an instance of the User class and calling save():
ross = User(email='ross@example.com', first_name='Ross', last_name='Lawley').save()
ross by setting attributes, rather than providing keyword arguments to the constructor:
ross = User(email='ross@example.com')
ross.first_name = 'Ross'
ross.last_name = 'Lawley'
ross.save()
You may notice that we have not specified a location for the User documents. This is because MongoEngine determines the collection from the class name (it will be user in this case). The id field is added automatically and is set to a MongoDB ObjectId when the document is first saved.
Now let's add a couple of posts:
post1 = TextPost(title='Fun with MongoEngine', author=ross)
post1.content = 'Took a look at MongoEngine today, looks pretty cool.'
post1.tags = ['mongodb', 'mongoengine']
post1.save()
post2 = LinkPost(title='MongoEngine Documentation', author=ross)
post2.link_url = 'http://docs.mongoengine.org/'
post2.tags = ['mongoengine']
post2.save()
save() again will update the document in the database accordingly.
1.4. Accessing our data
So now we've got two posts in our database. Let's see how we can query them. The objects attribute of a Document class is the interface through which database queries are made. For our first query let's get all the posts in the database:
for post in Post.objects:
print(post.title)
1.4.1. Retrieving type-specific information
This will print the titles of our posts, one on each line. To retrieve type-specific information, you can query the subclass directly:
for post in TextPost.objects:
print(post.content)
for post in LinkPost.objects:
print(post.link_url)
There are also more advanced ways of getting type-specific information. For example, we might want to display all the posts, but include the content if it is a text post, and the link if it is a link post:
for post in Post.objects:
if isinstance(post, TextPost):
print(post.content)
elif isinstance(post, LinkPost):
print(post.link_url)
1.4.2. Searching our posts by tag
MongoEngine supports a number of different query operators, and all queries are performed using Python objects. Let's say we want to find all posts that have been tagged with 'mongodb':
for post in Post.objects(tags='mongodb'):
print(post.title)
When you pass a value for a ListField as a query parameter, it will search for documents where the field contains that value. We can also use field lookup operators. Let's say we want to find all posts written by users with a first name starting with 'R':
Post.objects(author__first_name__startswith='R')
1.4.3. Learning more about MongoEngine
If you want to learn more about MongoEngine, the User Guide provides a comprehensive reference to all of the facilities available. There is also an API Reference if you need any lower-level information.
You can also find us on GitHub — if you have any issues or need to report a bug, please do so there.