Django for Designers/Adding models

From OpenHatch wiki
Revision as of 04:44, 12 March 2013 by imported>Paulproteus (Move models-y stuff here)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Part 3: Models, our database, and making it visible

Remember to make a new branch for section 3 based off the official branch!

# in django-for-designers/myproject
$ git branch my-branch-3 origin/pre-part-3
$ git checkout my-branch-3

Introduction to databases and the ORM, or: finally, something we couldn't've done with plain HTML/CSS/JS!

Okay, being able to design your URLs however you want, without them having to correspond to your actual file structure, is pretty neat. But besides that, we haven't done anything yet that you couldn't do with just HTML, CSS, and JavaScript. Time to fix that.

One key thing that separates web apps from web pages is that apps typically store data somewhere. The code that runs the app chooses what data seems most important to show at the moment. When writing an app using Django, we configure the storage through Django models.

Every Django app comes with a models.py in which you list each kind of data you want to store. These are configured through Python classes (to name the kind of data) with a sequence of attributes (which control the pieces of data that make up the model). If you're familiar with, or interested in, SQL, these correspond to tables and columns respectively.

The models that you define here are built on top of Django's object-relational mapper, or ORM. Because you define your models as Python objects, you can effectively write queries against a relational database by instead coding in Python with reference to your objects. Django then maps your Python code into relational database queries in SQL.

By configuring your data access in Python, Django makes it easy to take advantage of your data layout in other places. For example, you'll see later how the ORM makes it easy to automatically generate forms that ask for exactly the information you need. It is also generally convenient to write your entire app in one language, rather than being required to switch to SQL to do queries. The ORM also handles "escaping," which makes your SQL queries handle Unicode and other strange characters properly. These conveniences let you avoid mojibake-style data corruption and SQL injection attacks effortlessly.

TODO (models.py --> ORM --> DB chart)

Creating a basic model

Let's make it so we can store some real live data in our application! In our simple bookmarking app, we’ll create two models: Bookmarks and Tags. What information do we need to store about these objects?

A bookmark has:

  • A URL
  • A title (optionally)
  • A timestamp for when the bookmark was made

A tag has:

  • The tag slug/name

A tag also needs to know which bookmarks it applies to.

How do we represent this information in a Django model?

Open up bookmarks/models.py in your editor.

from django.db import models


class Bookmark(models.Model):
    url = models.URLField()
    timestamp = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=200, blank=True, default="")

What's going on here?

  • We've made a Bookmark class that inherits from Django's model class.
  • We gave it a url, which is a URLField (a field that expects a string that parses as a valid URL).
  • We gave it a timestamp, which is a DateTimeField (a field that expects a Python datetime object). auto_now_add means that the field will automatically set itself to the time when the model is created.
  • Finally, we gave it a title, which is a CharField (a field that expects a string of some length or less, in this case less than 200 characters). "blank=True" means that Django will permit this field to be blank. We also set a default of the empty string, so that if someone fails to specify a title we permit that.

Now let's add a tag model! In the same file, below the Bookmark model:

class Tag(models.Model):
    bookmark = models.ManyToManyField(Bookmark)
    slug = models.CharField(max_length=50, unique=True)

The tag slug is stored in another CharField, which we've seen before. But what's this ManyToManyField? Well, it's one type of field for denoting a relationship between two models.

In relational databases, there's two basic types of relationships. In a ForeignKey relationship, one model relates to one and only one other model. You could imagine this like a Car model and a Wheel model. Each Wheel instance belongs to one and only one Car, so it would have a ForeignKey field for its car. The car, of course, can have multiple wheels.

With a ManyToManyField, on the other hand, the relationship isn't exclusive for either of the models involved. For instance, you could imagine having a Pizza model and a Topping model. Each Pizza can have multiple Toppings, and each Topping can be on multiple Pizzas.

(There are other variants of these fields, but this is the basic concept. See https://docs.djangoproject.com/en/dev/ref/models/fields/#module-django.db.models.fields.related for more details for how these relationship fields work in Django.)

In this case, each Bookmark can have multiple Tags, and each Tag can apply to multiple Bookmarks. So we use a ManyToManyField to store that information.

Creating database tables for your app (carefully, with South)

When you first create your models, you might not always know exactly what fields your models will need in advance. Maybe someday your bookmarks app will have preview thumbnail images! Then you would want to add another field to the model to store that information. Maybe someday you'll decide that tracking all the timestamps is silly, and want to delete that field.

Unfortunately, Django (and most database-using software) can’t figure out how to handle model changes very well on its own. Fortunately, a Django app called South that we installed earlier can handle these database changes--called ‘migrations’--for us.

Now that we’ve made our first version of our models file, let’s set up our bookmarks app to work with South so that we can make migrations with it in the future!

On the command line, write:

# in django-for-designers/myproject
$ python manage.py schemamigration bookmarks --initial

As you can see, that’s created a migrations directory for us, and automatically made a new migration file inside it.

# in django-for-designers/myproject
$ ls bookmarks/migrations/
0001_initial.py  __init__.py

All we need to do now is apply our new migration:

# in django-for-designers/myproject
$ python manage.py migrate bookmarks

Great! Now our database file knows about bookmarks and its new models, and if we need to change our models, South is set up to handle those changes. We’ll come back to South later.

IMPORTANT: You can't migrate an app if it's already been synced in the database using

python manage.py syncdb

. But you do need to run syncdb at least once before you use south (since south itself uses syncdb to give itself space in your database). That's why it's super important that when you run syncdb, south should be listed under INSTALLED_APPS, but none of your own apps should be, and after you add your app to INSTALLED_APPS, you must not run syncdb again until after you've already set up migrations with that app.

Add and commit all your work, including the migrations folder that South generated for you!

Add some bookmarks via the command line

Now, let’s hop into the interactive Python shell and play around with the free API ("Application programming interface" -- APIs aren't just data doodads that hip web startups provide for you, they're an important concept in software architecture.) that Django gives you. To invoke the Python shell, use this command:

# in django-for-designers/myproject
$ python manage.py shell

We’re using this instead of simply typing “python”, because manage.py's shell sets up the project’s environment for you. “Setting up the environment” involves two things:

  • Making sure bookmarks (and any other apps you might have) are on the right path to be imported.
  • Setting the DJANGO_SETTINGS_MODULE environment variable, which gives Django the path to your settings.py file.

Once you’re in the shell, let's explore the database API. Let's import the model classes we just wrote:

>>> from bookmarks.models import Bookmark, Tag

List all the current Bookmarks:

>>> Bookmark.objects.all()
[]

How many bookmarks is this? It's an empty list, so zero!

Let's add a bookmark:

>>> b = Bookmark(url="http://www.bringinthecats.com/", title="Funny link")

We could specify the value for timestamp as well here, but we don't have to since when we wrote its model definition, we specified that timestamp has a default value (of when the bookmark is created). We did specify a title for the bookmark, even though we didn't have to since it's an optional field.

Try getting the ID number of our new bookmark 'b' by typing:

>>> b.id

Python has given you back your prompt because the value of b.id is None. That is because we have not yet saved the object; to the database, it does not exist.

Save your bookmark to the database. In Django, you have to call save() explicitly.

>>> b.save()

Now, try again to get the id of the Bookmark instance. Because it’s been saved, it has an ID in the database now! Even though we didn't specify one in our models.py, every saved model instance automatically has an id field.

>>> b.id
1

Access the database columns (Fields, in Django parlance) as Python attributes:

>>> b.title
"Funny link"
>>> b.timestamp
datetime.datetime(2011, 12, 1, 3, 3, 55, 841929)

We can change the bookmark title by changing its title attribute, then calling save().

>>> b.title = "WHEEEE"
>>> b.save()
>>> b.title
"WHEEEE"

If we now ask Django to show a list of all the Bookmark objects available, we can see it's no longer an empty list!

>>> Bookmark.objects.all()
[<Bookmark: Bookmark object>]

Fix the hideous default model representation=

Wait a minute! <Bookmark: Bookmark object> is an utterly unhelpful representation of this object. Let’s fix that by editing the Bookmark model. Use your text editor to open the bookmarks/models.py file and adding a __unicode__() method to both Bookmark and Tag:

class Bookmark(models.Model):
    url = models.URLField()
    timestamp = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=200, blank=True, default="")

    def __unicode__(self):
        return self.url


class Tag(models.Model):
    bookmark = models.ManyToManyField(Bookmark)
    slug = models.CharField(max_length=50, unique=True)

    def __unicode__(self):
        return self.slug

Let's see our new shiny __unicode__ methods in action. Start a new Python interactive shell by running:

# in django-for-designers/myproject
$ python manage.py shell
>>> from bookmarks.models import Bookmark, Tag
>>> Bookmark.objects.all()
[<Bookmark: http://www.bringinthecats.com/>]

Save and commit your changes.

Adding more data via the shell

>>> b = Bookmark(url="http://www.google.com")
>>> b.save()
>>> b = Bookmark(url="https://us.pycon.org/2013/", title="PyCon US website")
>>> b.save()
>>> b = Bookmark(url="https://www.djangoproject.com")
>>> b.save()

We've created a bunch of bookmarks, but no tags! Let's change that.

>>> b = Bookmark.objects.all()[0]
>>> b
<Bookmark: http://www.bringinthecats.com/>
>>> b.tag_set.create(slug="cats")
<Tag: cats>
>>> b.tag_set.create(slug="music")
<Tag: music>
>>> b.tag_set.create(slug="funny")
<Tag: funny>
>>> b.save()

Slicing and dicing Django data

There are many methods for searching across your Django models.

We can filter our bookmarks, for instance for ones with a particular URL:

>>> foo = Bookmark.objects.filter(url="http://www.bringinthecats.com/")
>>> foo
[<Bookmark: http://www.bringinthecats.com/>]

Or for bookmarks which have titles:

>>> titled_bookmarks = Bookmark.objects.exclude(title="")
>>> titled_bookmarks
[<Bookmark: http://www.bringinthecats.com/>, <Bookmark: https://us.pycon.org/2013/>]

If you try to use filter to search for a question that does not exist, filter will give you the empty list.

>>> Bookmark.objects.filter(title="Who framed Roger Rabbit?")
[]

The get method, on the other hand, returns exactly one hit. If it finds zero matches, or more than one match, it will raise an exception.

>>> Bookmark.objects.get(id=1)
<Bookmark: http://www.bringinthecats.com/>
>>> Bookmark.objects.get(id=4)
Traceback (most recent call last):
    ...
DoesNotExist: Bookmark matching query does not exist.

More information on making queries with Django's ORM can be found in the Django docs at https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-objects.