[Solved]-Django and models with multiple foreign keys

25👍

There is plenty of room for improvement. By using through on ManyToManyField you can explicitly define the join table, which we can conveniently consider as a single visit to a city during a particular trip. During that visit we had activities, so activity should have a foreignkey to a visit.

For each foreignkey in a table, Django will add API convenience manager for sets of objects on the opposite side of the relationship. Destination will have a visit_set, but so will Trip. Similarly, because of visit foreignkey in Activity each visit will have an activity_set.

First start with the models:

from django.db import models

# Create your models here.
class Destination(models.Model):
    city_name=models.CharField(max_length=50)

class Trip(models.Model):
    departing_on=models.DateField()
    returning_on=models.DateField()
    destinations=models.ManyToManyField(Destination, through='Visit')

class Visit(models.Model):
    destination=models.ForeignKey(Destination)
    trip=models.ForeignKey(Trip)

class Activity(models.Model):
    name=models.CharField(max_length=50)
    visit=models.ForeignKey(Visit)

Then lets change list_trip a bit, added print_trip for clarity of what is going on in template:

def list_trip(request, template_name = 'trip-list.html'):
    return render_to_response(template_name, {
        'page_title': 'List of trips',
        'trips': Trip.objects.all(),
        })

def print_trips():
    for trip in Trip.objects.all():
        for visit in trip.visit_set.select_related().all():
            print trip.id, '-', visit.destination.city_name
            for act in visit.activity_set.all():
                print act.name

And finally the improved template:

{% block content %}
    {% for trip in trips %}
        {{ trip.id }} - {{ trip.name }}

        {% for visit in trip.visit_set.select_related.all %}
            {{ visit.destination.city_name }}

            {% for act in visit.activity_set.all %}
                 {{ act.name }}
            {% endfor %}
        {% endfor %}
    {% endfor %}
{% endblock %}

There is still some more room for improvement performance wise. Notice I used select_related. That will prefetch all destinations at the time visits are fetched, so that visit.destination.city_name will not incur another db call. However this doesn’t work for reverse ManyToMany relationships (in our case all members of activity_set). Django 1.4 will come out with new method called prefetch_related which will resolve that as well.

In the mean time, read up on Efficient reverse lookups for an idea how to even further reduce the number of DB hits. In the comments few readily available solutions are mentioned as well.

Leave a comment