Working with models, part 1

Published November 3, 2007. Filed under: Django.

In order to do all the things it does, Django has to contain quite a bit of generic code that’s capable of working with pretty much any model or combination of models you want to throw at it, and has to be able to introspect those models to get information about their fields, their relationships and other useful tidbits. There are also plenty of real-world use cases where your code needs to be able to do the same sorts of things, and so knowing how Django does it — and how you can take advantage of the machinery it already provides — is an important step to becoming a fluent Django user. So in this and the next couple of tips we’ll take a look at some of those features, and how you can put them to use in your own applications.

Querying on any of multiple models

There are all sorts of cases where you need to be able to do a “generic” sort of lookup without knowing in advance what model you’ll be working with. If all you need is to have a view which displays a list of objects or detail of one particular object, Django’s generic views will be all you need, but that’s really a smaller subset of a larger problem; if you’re writing a template tag, for example, a generic view can’t help you.

One solution — the one used by generic views — is to write functions which take a QuerySet as an argument, which neatly sidesteps the problem of “which model do I use” by shifting it onto the person who’s using the generic view. If you don’t mind doing that, it’s an easy and effective way to deal with this problem, since the QuerySet API is the same no matter what model the QuerySet comes from: you can always use filter, get, etc., no matter what model you’re looking at.

Another solution would be to make up a list of models and related information. If you know that the set of models you might be working with is going to be fairly small and relatively constant, this is workable; one common case is working with comments, where you can be fairly certain that you’ll only ever have to deal with the Comment and FreeComment models in django.contrib.comments. For example, comment_utils relies on this to provide a consistent way to take a single comment and get all comments of that type posted by the same person; it uses a simple dictiionary which generates the appropriate lookup arguments:

kwarg_builder = {
    Comment: lambda c: { 'user__username__exact': c.user.username },
    FreeComment: lambda c: { 'person_name__exact': c.person_name },
}

Then it’s a simple matter, given a comment object which might be from either model, to get all comments of that type from the same person:

comment_class = comment_obj.__class__
person_kwargs = kwarg_builder[comment_class](comment)
comment_list = comment_class.objects.filter(**person_kwargs)

(the double asterisks, in case you’re wondering, are Python’s syntax for taking a dictionary and turning it into keyword arguments to a function)

But this still isn’t ideal; you need to maintain a hard-coded list of the models you want to work with, and the relevant information you need for each one. What we really want here is a generic solution.

Loading and querying on any model

The solution is to use Django’s model-loading system, which lives in django.db.models.loading but has some useful things which get imported up into the django.db.models namespace. The most important one, for what we’re considering here, is the function get_model(), which takes two pieces of information — an “app label” and “model name” — and returns the model which matches them. The app label is simply the (normalized to lower-case) name of the application the model lives in, and the model name is the (also normalized to lower case) name of the model class. So, for example, the User model in django.contrib.auth has an app label of “auth” and a model name of “user”. Given this, you can get at the User model without having to import it:

from django.db.models import get_model
user_model = get_model('auth', 'user')

From there you can query on it just as if you’d imported it directly:

active_users = user_model.objects.filter(is_active=True)

The place where this trick really shines is cases where you know you’re going to work with some model, just not which model; as long as something, somewhere, can generate two strings — the app label and model name — get_model() can get the model class for you. It’s somewhat conventional to pass these as a single string with a dot between them (so, in the case of the User model, “auth.user”), and then just split it and pass to get_model() for the result:

model_class = get_model(*model_string.split('.'))

(the single asterisk here is the Python syntax for taking a list and turning it into positional arguments to a function)

For a real-life example, the generic content-retrieval tags in template_utils use this to open up the ability to dynamically retrieve objects from any model you have installed; one of the arguments to each of those tags is a string consisting of an app label, a dot and a model name, which means you can use any of these tags like so:

{% load generic_content %}
{% get_latest_object comments.freecomment as latest_comment %}

As you might guess, this fetches the latest FreeComment out of your database, by using get_model() to fetch the model class, and then accesses the model’s default manager to do the query.

It’s worth emphasizing the “default manager” bit there; it’d be tempting to write the code like this, for example:

model_class = get_model(*model_str.split('.'))
latest = model_class.objects.latest()

But this might end up crashing with an AttributeError, because Django lets you define custom managers with any names you like, and doesn’t actually require that the default manager be named “objects”. When the default manager isn’t named “objects”, trying to access model_class.objects simply won’t work, so you need to use the special attribute _default_manager, which Django always sets to the default manager for the model regardless of what that manager is called:

model_class = get_model(*model_str.split('.'))
latest = model_class._default_manager.latest()

In the examples above with hard-coded information about the comments models, this wasn’t a problem because we were deliberately limiting the set of possible models to just two, both of which have default managers named “objects”, but if you’re going to write truly generic code this is something you need to be aware of.

You’re soaking in it

The generic model-loading ability provided by get_model() is used in several places in Django:

Other model-loading goodies

While get_model() is really the most useful thing you’ll find in Django’s model-loading code, there are a few other things in there you’ll probably want to know about (all of which can be imported from django.db.models):

For an example of how these can be useful, consider the main page of the admin app, which displays a big list of applications and associdated models. That’s actually implemented as a template tag (in django.contrib.admin.templatetags.adminapplist), and uses a pretty simple technique to build up the list: first it calls get_apps() to get the list of modules with models, then loops through that list calling get_models() on each one to get the list of its models and determine whether to display them.

There are a few more functions in Django’s model-loading module, but they’re really only useful for Django’s own internal use (and all of the functions listed above are actually implemented as methods on a class called AppCache, which serves as a registry of your project’s available models).

Choose your weapon

Depending on exactly what you need to do, any of the methods above — taking a QuerySet argument, building a list of models to work with, or using Django’s own generic model-loading code — can be useful, so keep them in mind for situations where you won’t always know in advance what model you’ll be working with. Tomorrow we’ll look at how to go from having the model class to getting useful information about it, so stay tuned.