Django and models with multiple foreign keys

23,177

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.

Share:
23,177
Martin
Author by

Martin

Updated on February 26, 2020

Comments

  • Martin
    Martin about 4 years

    I am new to Django and I've been impressed so far by its capabilities. I am playing with more complex models and I am have problem to use them properly. Using Django 1.3, I am trying to write a summary page which would present the three models below with the following structure. In other words, a list of trips with their destinations and activities.

    • Trip 1
      • Destination 1
      • Destination 2
      • Activity 1
    • Trip 2
      • Destination 1
      • Activity 2

    Models

    • Trip <-> TripDestination <-> Destination (a trip can have multiple destinations)
    • Activity -> Trip, Activity -> Destination (an activity is defined for a trip at a specific location/destination)
        class Destination(models.Model):
            city_name=models.CharField()
    
        class Trip(models.Model):
            departing_on=models.DateField()
            returning_on=models.DateField()
            destinations=models.ManyToManyField(Destination)
    
        class Activity(models.Model):
            destination=models.ForeignKey(Destination, null=False)
            trip=models.ForeignKey(Trip, null=False)

    I am trying to write a view which would generate a page with the structure presented above. The main problem I am having right now is to display the activities for a specific trip and destination. As you can see in the code below, I am building a dictionary and I doubt it is the right thing to do. In addition, the view becomes

    View

    def list_trip(request, template_name = 'trip-list.html'):
        trips = Trip.objects.all()
    
        # Build a dictionary for activities -- Is this the right thing to do?
        activities = Activity.objects.filter(trip__in=trips)
        activities_by_trips = dict()
        for activity in activities:
            if activity.trip_id not in activities_by_trips:
                activities_by_trips[activity.trip_id] = dict()
    
            if activity.destination_id not in activities_by_trips[activity.trip_id]:
                activities_by_trips[activity.trip_id][activity.destination_id] = []
    
            activities_by_trips[activity.trip_id][activity.destination_id].append(activity)
    
        return render_to_response(template_name, {
            'page_title': 'List of trips',
            'trips': trips,
            'activities_by_trips': activities_by_trips,
        })

    Template

    
    {% block content %}
        {% for trip in trips %}
            {{ trip.id }} - {{ trip.name }}
    
            {% for destination in trip.destinations.all %}
                {{ destination.city_name }}
    
                ** This is terrible code -- How to fix that **
                {% for key, value in activities_by_trips|dict_lookup:trip.id %}
                    {% if value %}
                        {% for key_prime, value_prime in value|dict_lookup:destination.id %}
                           {{ value_prime.description }}
                        {% endfor %}
                    {% endif %}
                {% endfor %}
            {% endfor %}
        {% endfor %}
    {% endblock %}
    

    In brief, can someone please help me to get a summary of all the trips and activities? What's the best way to accomplish that? Is the model correct?

    Thanks!

  • Martin
    Martin about 12 years
    Thank you digivampire! This is exactly what I was looking for.