"
This article is part of in the series
Published: Thursday 4th April 2013
Last Updated: Wednesday 29th December 2021

The previous article Writing Your First Python Django Application is a step-by-step guide on how to write a simple Django application from scratch. In this article, you will learn how to write models for your new Django application.

Software Architectural Patterns

Before we dive into the code, let's review two of the most popular server-side software architectural design patterns: Model-View-Controller and Presentation-Abstraction-Control.

Model-View-Controller

The Model-View-Controller (MVC) design pattern is a software architecture pattern which separates the presentation of data from the logic of handling user interactions. A model specifies what kind of data gets stored. A view requests data from a model and generates outputs from it. A controller provides logic to change the view's presentation or update the model's data.

Presentation-Abstraction-Control

Like MVC, Presentation-Abstraction-Control (PAC) is another popular software architectural pattern. PAC separates the system into layers of components. Within each layer, the presentation component generates output from the input data; the abstraction component retrieves and processes data; and the control component is the middleman between presentation and abstraction which manages the flow of information and the communication between these components. Unlike MVC, where a view talks directly to a model, PAC's presentation and abstraction never talk directly to each other and the communication between them is mediated by control. Unlike Django which follows the MVC pattern, the popular content management system Drupal follows the PAC pattern.

Django's MVC

Although Django adopts the MVC pattern, it is a little bit different from the standard definition. Namely that,

  • In Django, a model describes what kind of data gets stored on the server. So, it's similar to a standard MVC pattern's model.
  • In Django, a view describes which data gets returned to the users. While a standard MVC's view describes how the data is presented.
  • In Django, a template describes how the data gets presented to the users. So, it's similar to a standard MVC pattern's view.
  • In Django, a controller defines the mechanism provided by the framework: the code that routes a incoming request to an appropriate view. So, it's similar to a standard MVC pattern's controller.

Overall, Django diverges from the standard MVC pattern because it suggests that a view should include business logic instead of only presentation logic as in the standard MVC, and that a template should take care of the majority of the presentation logic while the standard MVC does not include a template component at all. Due to these differences of Django's design compared to the standard MVC, we usually call Django's design Model-Template-View + Controller where Controller is often omitted because it's part of the framework. Therefore, most of the time Django's design pattern is called MTV.

Although it's helpful to understand the design philosophy of Django's MTV pattern, at the end of the day the only thing that matters is getting the job done and Django's ecosystem provides everything geared towards programming efficiency.

Creating Models

Since the new Django application is a blog, we are going to write two models, Post and Comment. A Post has a content field and a created_at field. A Comment has a message field and a created_at field. Each Comment is associated with a Post.

[python]
from django.db import models as m

class Post(m.Model):
content = m.CharField(max_length=256)
created_at = m.DateTimeField('Datetime created')

class Comment(m.Model):
post = m.ForeignKey(Post)
message = m.TextField()
created_at = m.DateTimeField('Datetime created')
[/python]

Next, modify the INSTALLED_APP tuple in myblog/settings.py to add myblog as an installed application.

[python]
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
# Uncomment the next line to enable the admin:
# 'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'myblog', # Add this line
)
[/python]

Now, you should be able to execute the following command to see what kind of raw SQL will be executed when you run syncdb. The command syncdb creates the database tables for all apps in INSTALLED_APPS whose tables haven't already been created yet. Behind the scenes, syncdb outputs raw SQL statements into the backend database management system (MySQL or PostgreSQL in our example).

[shell]
$ python manage.py sql myblog
[/shell]


[shell]
BEGIN;
CREATE TABLE `myblog_post` (
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`content` varchar(256) NOT NULL,
`created_at` datetime NOT NULL
)
;
CREATE TABLE `myblog_comment` (
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`post_id` integer NOT NULL,
`message` longtext NOT NULL,
`created_at` datetime NOT NULL
)
;
ALTER TABLE `myblog_comment` ADD CONSTRAINT `post_id_refs_id_648c7748` FOREIGN KEY (`post_id`) REFERENCES `myblog_post` (`id`);

COMMIT;
[/shell]



[shell]
BEGIN;
CREATE TABLE "myblog_post" (
"id" serial NOT NULL PRIMARY KEY,
"content" varchar(256) NOT NULL,
"created_at" timestamp with time zone NOT NULL
)
;
CREATE TABLE "myblog_comment" (
"id" serial NOT NULL PRIMARY KEY,
"post_id" integer NOT NULL REFERENCES "myblog_post" ("id") DEFERRABLE INITIALLY DEFERRED,
"message" text NOT NULL,
"created_at" timestamp with time zone NOT NULL
)
;

COMMIT;
[/shell]


The SQL dump looks good! Now, you can create the tables in the database by executing the following command.

[shell]
$ python manage.py syncdb
Creating tables ...
Creating table myblog_post
Creating table myblog_comment
Installing custom SQL ...
Installing indexes ...
Installed 0 object(s) from 0 fixture(s)
[/shell]

Notice that two tables, myblog_post and myblog_comment, are created in the previous command.

Have fun with the new models

Now let's dive into the Django shell and have fun with our brand new models. To run our Django application in interactive mode, type the following command:

[shell]
$ python manage.py shell
Python 2.7.2 (default, Oct 11 2012, 20:14:37)
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>
[/shell]

The interactive shell opened by the previous command is a normal Python interpreter shell in which you can freely execute statements against our Django application.

[python]
>>> from myblog import models as m
>>> # No post in the database yet
>>> m.Post.objects.all()
[]
>>> # No comment in the database yet
>>> m.Comment.objects.all()
[]
>>> # Django's default settings support storing datetime objects with tzinfo in the database.
>>> # So we use django.utils.timezone to put a datetime with time zone information into the database.
>>> from django.utils import timezone
>>> p = m.Post(content='Django is awesome.', created_at=timezone.now())
>>> p
<Post: Post object>
>>> p.created_at
datetime.datetime(2013, 3, 26, 17, 6, 39, 329040, tzinfo=<UTC>)
>>> # Save / commit the new post object into the database.
>>> p.save()
>>> # Once a post is saved into the database, it has an id attribute which is the primary key of the underlying database record.
>>> p.id
1
>>> # Now we create another post object without saving it into the database.
>>> p2 = m.Post(content='Pythoncentral is also awesome.', created_at=timezone.now())
>>> p2
<Post: Post object>
>>> # Notice p2.id is None, which means p2 is not committed into the database yet.
>>> p2.id is None
True
>>> # Now we retrieve all the posts from the database and inspect them just like a normal python list
>>> m.Post.objects.all()
[<Post: Post object>]
>>> m.Post.objects.all()[0]
<Post: Post object>
>>> # Since p2 is not saved into the database yet, there's only one post whose id is the same as p.id
>>> m.Post.objects.all()[0].id == p.id
True
>>> # Now we save / commit p2 into the database and re-run the query again
>>> p2.save()
>>> m.Post.objects.all()
[<Post: Post object>, <Post: Post object>]
>>> m.Post.objects.all()[1].id == p2.id
True
[/python]

Now we're familiar with the new Post model, how about using it side-by-side with the new Comment. A Post can have multiple Comments while a Comment can have only one Post.

[python]
>>> c = m.Comment(message='This is a comment for p', created_at=timezone.now())
>>> c.post = p
>>> c.post
<Post: Post object>
>>> c.post.id == p.id
True
>>> # Since c is not saved yet, p.comment_set.all() does not include it.
>>> p.comment_set.all()
[]
>>> c.save()
>>> # Once c is saved into the database, p.comment_set.all() will have it.
>>> p.comment_set.all()
[<Comment: Comment object>]
>>> p.comment_set.all()[0].id == c.id
True
>>> c2 = m.Comment(message='This is another comment for p.', created_at=timezone.now())
>>> # If c2.post is not specified, then Django will raise a DoseNotExist exception.
>>> c2.post
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/Users/xiaonuogantan/python2-workspace/lib/python2.7/site-packages/django/db/models/fields/related.py", line 389, in __get__
raise self.field.rel.to.DoesNotExist
DoesNotExist
>>> # Assign Post p to c2.
>>> c2.post = p
>>> c2.save()
>>> p.comment_set.all()
[<Comment: Comment object>, <Comment: Comment object>]
>>> # Order the comment_set according Comment.created_at
>>> p.comment_set.order_by('created_at')
[<Comment: Comment object>, <Comment: Comment object>]
[/python]

So far, we know how to create, save and retrieve Post and Comment using existing attributes of each model. How about querying the database to find the posts and comments we want? It turns out that Django provides a slightly peculiar syntax for querying. Basically a filter() function accepts arguments who conform to the form "[field]__[field_attribute]__[relationship]=[value]". For example,

[python]
>>> # Retrieve a list of comments from p.comment_set whose created_at.year is 2013
>>> p.comment_set.filter(created_at__year=2013)
[<Comment: Comment object>, <Comment: Comment object>]
>>> # Retrieve a list of comments from p.comment_set whose created_at is later than timezone.now()
>>> p.comment_set.filter(created_at__gt=timezone.now())
[]
>>> # Retrieve a list of comments from p.comment_set whose created_at is earlier than timezone.now()
>>> p.comment_set.filter(created_at__lt=timezone.now())
[<Comment: Comment object>, <Comment: Comment object>]
>>> # Retrieve a list of comments from p.comment_set whose message startswith 'This is a '
>>> p.comment_set.filter(message__startswith='This is a')
[<Comment: Comment object>, <Comment: Comment object>]
>>> # Retrieve a list of comments from p.comment_set whose message startswith 'This is another'
>>> p.comment_set.filter(message__startswith='This is another')
[<Comment: Comment object>]
>>> # Retrieve a list of posts whose content startswith 'Pythoncentral'
>>> m.Post.objects.filter(content__startswith='Pythoncentral')
[<Post: Post object>]
>>> # Retrieve a list of posts which satisfies the query that any comment in its comment_set has a message that startswith 'This is a'
>>> m.Post.objects.filter(comment__message__startswith='This is a')
[<Post: Post object>, <Post: Post object>]
>>> # Retrieve a list of posts which satisfies the query that any comment in its comment_set has a message that startswith 'This is a' and a created_at that is less than / earlier than timezone.now()
>>> m.Post.objects.filter(comment__message__startswith='This is a', comment__created_at__lt=timezone.now())
[<Post: Post object>, <Post: Post object>]
[/python]

Did you notice something odd about the last two queries? Isn't it strange that m.Post.objects.filter(comment__message__startswith='This is a') and m.Post.objects.filter(comment__message__startswith='This is a', comment__created_at__lt=timezone.now()) return two Post instead of one? Let's verify what posts have been returned.

[python]
>>> posts = m.Post.objects.filter(comment__message__startswith='This is a')
>>> posts[0].id
1
>>> posts[1].id
1
[/python]

Ah hah! posts[0] and posts[1] are the same post! How did that happen? Well, since the original query is a join query of Post and Comment and there are two Comment satisfying the query, two Post objects are returned. So, how do we make it so that only one Post is returned? It's simple, just append a distinct() to the end of the filter():

[python]
>>> m.Post.objects.filter(comment__message__startswith='This is a').distinct()
[<Post: Post object>]
>>> m.Post.objects.filter(comment__message__startswith='This is a', comment__created_at__lt=timezone.now()).distinct()
[<Post: Post object>]
[/python]

Summary and suggestions

In this article, we wrote two simple models Post and Comment for our blog website. Instead of writing raw SQL, Django provides a powerful and easy-to-use ORM that allows us to write succinct and easy-to-maintain database manipulation code. Instead of stopping here, you should dive into the code and run python manage.py shell to interact with the existing Django models. It's a lot of fun!

About The Author

Xiaonuo Gantan