Added docs directory, started tutorial
This commit is contained in:
259
docs/tutorial.rst
Normal file
259
docs/tutorial.rst
Normal file
@@ -0,0 +1,259 @@
|
||||
Tutorial
|
||||
========
|
||||
This tutorial introduces **MongoEngine** by means of example --- we will walk
|
||||
through how to create a simple **Tumblelog** application. A Tumblelog is a type
|
||||
of blog where posts are not constrained to being conventional text-based posts.
|
||||
As well as text-based entries, users may post images, links, videos, etc. For
|
||||
simplicity's sake, we'll stick to text, image and link entries in our
|
||||
application. As the purpose of this tutorial is to introduce MongoEngine, we'll
|
||||
focus on the data-modelling side of the application, leaving out a user
|
||||
interface.
|
||||
|
||||
Connecting to MongoDB
|
||||
---------------------
|
||||
Before we start, you should make sure that you have a copy of MongoDB running
|
||||
in an accessible location --- running it locally will be easier, but if that is
|
||||
not an option then it may be run on a remote server.
|
||||
|
||||
Before we can start using MongoEngine, we need to tell it how to connect to our
|
||||
instance of **mongod**. For this we use the :func:`mongoengine.connect`
|
||||
function. The only argument we need to provide is the name of the MongoDB
|
||||
database to use::
|
||||
|
||||
from mongoengine import *
|
||||
|
||||
connect('tumblelog')
|
||||
|
||||
This will connect to a mongod instance running locally on the default port. To
|
||||
connect to a mongod instance running elsewhere, we may specify the host and
|
||||
port explicitly::
|
||||
|
||||
connect('tumblelog', host='192.168.1.35', port=12345)
|
||||
|
||||
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 it comes to
|
||||
migrations. However, defining schemata 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 :abbr:`ORMs
|
||||
(Object-Relational Mappers)` do.
|
||||
|
||||
In our Tumblelog application we need to store several different types of
|
||||
information. We will need to have a collection of **users**, so that we may
|
||||
link posts to an individual. We also need to store our different types
|
||||
**posts** (text, image and link) in the database. For to aid navigation of our
|
||||
Tumblelog, posts may have **tags** associated with them, so that the list of
|
||||
posts shown to the user may be limited to posts that have a specified tag.
|
||||
Finally, it would be nice if **comments** could be added to posts. We'll start
|
||||
with **users**, as the others are slightly more involved.
|
||||
|
||||
Users
|
||||
^^^^^
|
||||
Just as if we were using a relational database with an ORM, we need to define
|
||||
which fields a :class:`User` may have, and what their types will be::
|
||||
|
||||
class User(Document):
|
||||
email = StringField(required=True)
|
||||
first_name = StringField(max_length=50)
|
||||
last_name = StringField(max_length=50)
|
||||
|
||||
This looks similar to how a the structure of a table would be defined in a
|
||||
regular ORM. The key difference is that this schema will never be passed on to
|
||||
MongoDB --- this will only be enforced at the application level. Also, the User
|
||||
documents will be stored in a MongoDB *collection* rather than a table.
|
||||
|
||||
Posts, Comments and Tags
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Well that wasn't too bad, was it? 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.
|
||||
|
||||
Posts
|
||||
"""""
|
||||
But MongoDB *isn't* a relational database, so we're not going to do it that
|
||||
way. As it turns out, we can use MongoDB's schemaless nature to provide us with
|
||||
a much nicer solution. We will store all of the posts in *one collection* ---
|
||||
each post type will just have the fields it needs. If we later want to add
|
||||
video posts, we don't have to modify the collection at all, we just *start
|
||||
using* the new fields we need to support video posts. This fits with the
|
||||
Object-Oriented principle of *inheritance* nicely. We can think of
|
||||
:class:`Post` as an base class, and :class:`TextPost`, :class:`ImagePost` and
|
||||
:class:`LinkPost` as subclasses of :class:`Post`. In fact, MongoEngine supports
|
||||
this kind of modelling out of the box::
|
||||
|
||||
class Post(Document):
|
||||
title = StringField(max_length=120, required=True)
|
||||
author = ReferenceField(User)
|
||||
|
||||
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
|
||||
:class:`mongoengine.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.
|
||||
|
||||
Tags
|
||||
""""
|
||||
Now that we have our Post models figured out, how will we attach tags to them?
|
||||
MongoDB allows us to store lists of items natively, so rather than having a
|
||||
link table, we can just store a list of tags in each post. Also, for both
|
||||
efficiency and simplicity's sake, we'll store the tags as strings directly
|
||||
within the post, rather than storing references to tags in a separate
|
||||
collection. Especially as tags are generally very short (often even shorted
|
||||
than a document's id), this denormalisation won't impact very strongly on the
|
||||
size of our database. So let's take a look that the code our modified
|
||||
:class:`Post` class::
|
||||
|
||||
class Post(Document):
|
||||
title = StringField(max_length=120, required=True)
|
||||
author = ReferenceField(User)
|
||||
tags = ListField(StringField(max_length=30))
|
||||
|
||||
The :class:`mongoengine.ListField` object that is used to define a Post's tags
|
||||
takes a field object as its first argument --- this means that you can have
|
||||
lists of any type of field (including lists). Note that we don't need to
|
||||
modify the specialised post types as they all inherit from :class:`Post`.
|
||||
|
||||
Comments
|
||||
""""""""
|
||||
A comment is typically associated with *one* post. In a relational database, to
|
||||
display a post with its comments, we would have to retrieve the post from the
|
||||
database, then query the database again for the comments associated with the
|
||||
post. This works, but there is no real reason to be storing the comments
|
||||
separately from their associated posts, other than to work around the
|
||||
relational model. Using MongoDB we can store the comments as a list of
|
||||
*embedded documents* directly on the post document. An embedded document should
|
||||
be treated no differently that a regular document; it just doesn't have its own
|
||||
collection. 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))
|
||||
|
||||
Adding data to our Tumblelog
|
||||
----------------------------
|
||||
Now that we've defined how our documents will be structured, lets start adding
|
||||
some documents to the database. Firstly, we'll need to create a :class:`User`
|
||||
object::
|
||||
|
||||
john = User(email='jdoe@example.com', first_name='John', last_name='Doe')
|
||||
john.save()
|
||||
|
||||
Simple, eh? Note that only fields with ``required=True`` need to be specified
|
||||
in the constructor, we could have also defined our user using attribute
|
||||
syntax::
|
||||
|
||||
john = User(email='jdoe@example.com')
|
||||
john.first_name = 'John'
|
||||
john.last_name = 'Doe'
|
||||
john.save()
|
||||
|
||||
Now that we've got our user in the database, lets add a couple of posts::
|
||||
|
||||
post1 = TextPost(title='Fun with MongoEngine', author=john)
|
||||
post1.content = 'Took a look at MongoEngine today, looks pretty cool.'
|
||||
post1.tags = ['mongodb', 'mongoengine']
|
||||
post1.save()
|
||||
|
||||
post2 = LinkPost(title='MongoEngine Documentation', author=john)
|
||||
post2.link_url = 'http://tractiondigital.com/labs/mongoengine/docs'
|
||||
post2.tags = ['mongoengine']
|
||||
post2.save()
|
||||
|
||||
Note that if you change a field on a object that has already been saved, then
|
||||
call :meth:`save` again, the document will be updated.
|
||||
|
||||
Accessing our data
|
||||
------------------
|
||||
So now we've got a couple of posts in our database, how do we display them?
|
||||
Each document class (i.e. any class that inherits either directly or indirectly
|
||||
from :class:`mongoengine.Document`) has an :attr:`objects` attribute, which is
|
||||
used to access the documents in the database associated with that class. So
|
||||
lets see how we can get our posts' titles::
|
||||
|
||||
for post in Post.objects:
|
||||
print post.title
|
||||
|
||||
Retrieving type-specific information
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
This will print the titles of our posts, one on each line. But What if we want
|
||||
to access the type-specific data (link_url, content, etc.)? One way is simply
|
||||
to use the :attr:`objects` attribute of a subclass of :class:`Post`::
|
||||
|
||||
for post in TextPost.objects:
|
||||
print post.content
|
||||
|
||||
Using TextPost's :attr:`objects` attribute only returns documents that were
|
||||
created using :class:`TextPost`. Actually, there is a more general rule here:
|
||||
the :attr:`objects` attribute of any subclass of :class:`mongoengine.Document`
|
||||
only looks for documents that were created using that subclass or one of its
|
||||
subclasses.
|
||||
|
||||
So how would we display all of our posts, showing only the information that
|
||||
corresponds to each post's specific type? As you might have guessed, there is a
|
||||
better way than just using each of the subclasses individually. When we used
|
||||
:class:`Post`'s :attr:`objects` attribute earlier, the objects being returned
|
||||
weren't actually instances of :class:`Post` --- they were instances of the
|
||||
subclass of :class:`Post` that matches the post's type. Lets look at how this
|
||||
works in practice::
|
||||
|
||||
for post in Post.objects:
|
||||
print post.title
|
||||
print '=' * len(post.title)
|
||||
|
||||
if isinstance(post, TextPost):
|
||||
print post.content
|
||||
|
||||
if isinstance(post, LinkPost):
|
||||
print 'Link:', post.link_url
|
||||
|
||||
print
|
||||
|
||||
This would print the title of each post, followed by the content if it was a
|
||||
text post, and "Link: <url>" if it was a link post.
|
||||
|
||||
Searching our posts by tag
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
The :attr:`objects` attribute of a :class:`mongoengine.Document` is actually a
|
||||
:class:`mongoengine.QuerySet` object. This lazily queries the database only
|
||||
when you need the data. It may also be filtered to narrow down your query. Lets
|
||||
adjust our query so that only posts with the tag "mongodb" are returned::
|
||||
|
||||
for post in Post.objects(tags='mongodb'):
|
||||
print post.title
|
||||
|
||||
There are also methods available on :class:`mongoengine.QuerySet` objects that
|
||||
allow different results to be returned, for example, calling :meth:`first` on
|
||||
the :attr:`objects` attribute will return a single document, the first matched
|
||||
by the query you provide. Aggregation functions may also be used on
|
||||
:class:`mongoengine.QuerySet` objects::
|
||||
|
||||
num_posts = Post.objects(tags='mongodb').count()
|
||||
print 'Found % posts with tag "mongodb"' % num_posts
|
||||
|
||||
Reference in New Issue
Block a user