I’m writing an application that deals with some slightly complex relationships. There are several offices, and each office has several workers. Workers can have multiple projects, and each project can have multiple workers. In addition, each project can serve multiple clients.
Here’s what that’d look like in a Django models.py file:
class Office(models.Model): office_code = models.CharField(max_length=24, blank=True) street_num = models.CharField(max_length=24) street_name = models.CharField(max_length=64) bldg_no = models.CharField(max_length=12, blank=True) suite = models.CharField(max_length=12, blank=True) city = models.CharField(max_length=100) state = USStateField() # grandiose assumption zipcode = models.CharField(max_length=10) main_phone = PhoneNumberField() site_mgr = models.ForeignKey(User, unique=True) class Worker(models.Model): user = models.ForeignKey(User, unique=True) extension = models.CharField(max_length=8) office = models.ForeignKey(Office) class Client(models.Model): fname = models.CharField(max_length=64) lname = models.CharField(max_length=64) street_num = models.CharField(max_length=24, blank=True) street_name = models.CharField(max_length=128, blank=True) apt_no = models.CharField(max_length=24, blank=True) city = models.CharField(max_length=128, blank=True) state = USStateField(blank=True) zipcode = models.CharField(max_length=10, blank=True) class Project(models.Model): date_started = models.DateTimeField() worker = models.ManyToManyField(Worker) office = models.ForeignKey(Office) client = models.ManyToManyField(Client)
While writing the template for my worker detail page, I decided that I didn’t want to just list the projects for that worker, but I also wanted to list the clients for each project. I ran into a bit of an issue at first in doing this. I tried something like this:
{% block content %} <h2>Projects for {{object.user.first_name}} {{object.user.last_name}}</h2> <ul> {% for project in object.projects %} <li><a href="{{project.get_absolute_url}}">{{project.id}} (Opened: {{project.date_started.date}})</a> <ul> {% for obj in project.get_clients %} <li>{{obj.lname}}, {{obj.fname}}</li> {% endfor %} </ul> <h2>Clients for project {{project.id}}</h2> {% endfor %}</ul> {% endblock %}
Looking back at the models, you’ll note that there’s no “projects” attribute of the Worker class. There’s also no “get_clients” method for the Project class. After reading some forum and blog posts, I got the idea to add these to my models manually. It seems a lot of people solve similar issues this way, and I don’t believe it’s necessary, which is why I’m posting this. What I added to my models looked something like this:
### ### in Worker model ### def projects(self): return self.project_set.filter(worker = self.pk) ### ### in Project model ### def get_clients(self): return self.client_set.all()
Adding these to the models actually does solve the problem, but it’s reinventing the wheel. Perhaps at some point in history Django’s ORM didn’t have the functionality it does now, but these days Django takes care of accessing the objects of related entities for you through the use of the RelatedManager (for one-to-one, foreign key relationships) and the ManyRelatedManager (for many-to-many relationships).
When you create a ForeignKey field or ManyToMany field in a Django model, Django’s ORM becomes aware of the relationship, and implements lots of shortcuts to help you in managing/exploiting it.
After reading some online documentation (see the “Related Objects” area, for one), I was able to get all of the data I wanted into my template without adding a single character of code to my models, which is what I had hoped for. Here’s the right way to do this, unless I’m mistaken (please let me know the best way if I am):
{% block content %} <h2>Projects for {{object.user.first_name}} {{object.user.last_name}}</h2> <ul> {% for project in object.project_set.all %} <li><a href="{{project.get_absolute_url}}">{{project.id}} (Opened: {{project.date_started.date}})</a> <ul> <h2>Clients for project {{project.id}}</h2> {% for obj in project.client.all %} <li>{{obj.lname}}, {{obj.fname}}</li> {% endfor %} </ul> {% endfor %} </ul> {% endblock %}
I’ve replaced “object.projects” with “object.project_set.all”. Note that, in a Django template, unless you specify otherwise, the name of a single object passed to a template is “object”, so in this case, “object” is a “Worker” object. The Worker model makes no mention at all of projects, and yet I’m able to easily grab data about project objects. This is because Django’s ORM actually gives you access to related object data from either side of a relationship. Since the Project model has a ManyToMany field referencing Worker, you can access project data from the worker object by using “object._set.all”, where is replaced with the lower-cased name of the model that points to “object”. Hence, “object.project_set.all”.
Now, in the second case, I’ve replaced “project.get_clients” with “project.client.all”. The Project model directly contains a field named “client” that is a ForeignKey to the Client model. When this condition exists, Django will happily traverse the relationship for you by just referencing the model’s field directly! The “all” method is a standard method of any Manager object I’m aware of, and it’s inherited by the RelatedManager and ManyRelatedManager objects.
One interesting thing I found, too, was that there’s no mention of “RelatedManager” or “ManyRelatedManager” in the online Django documentation. This is highly unusual. In my experience, Django’s documentation blows away the docs for just about any project in existence. Did I miss something?