Many-to-Many Relations

Learning Objectives

  1. Use Django's ManyToManyField to enable a m:m relationship

  2. Use mainmodel.submodels and submodel.mainmodel_set to access related data from either Model.

Yesterday we learned the first of two common types of data relations: One-to-Many or 1:m. In this relationship, one model is said to "own" multiple objects of another model. It's a very common relationship and we can readily think of examples: one customer has many orders; one conference has many attendees; one restaurant has many menu items; one collector has many cats.

The next kind of relationship we will learn about is the Many-to-Many relationship, often abbreviated m:m or n:m. You can think of this as a two-way one-to-many relationship where each model "owns" multiple objects of the other model. Some examples of this include: one band can have many genres of music that they play, and one genre has many bands in it; one article has many category tags, and each category tag is linked to many articles; and today's example - one cat can have many toys, and each toy could be owned by multiple cats.

If we were writing all of this ourselves we would normally need an intermediary table, called a join table, to enable the references between tables. We use a join table because if we didn't, the nature of a many-to-many relationship would require one of our related tables to contain duplicate data. BAD!

We aren't writing it ourselves, though. (Not yet, anyway.) Django has a very easy way to link models together in a many-to-many relationship that takes care of the join table and references in the database for us. Let's see how it works.

Adding the CatToy Model

We are creating a new model to represent toys that our cat owns. We will call it CatToy. Each CatToy can be owned by many Cats and each Cat can own many CatToys. Open up your main_app/models.py and add a class for the CatToy model above the Cat model class:

class CatToy(models.Model):
    name = models.CharField(max_length=100)
    color = models.CharField(max_length=50)

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('cattoys_detail', kwargs={'cattoy_id': self.id})

We don't need to represent much data about the toys to set up the relationship. We've included a __str__ method so that the Model will nicely print its name. We also included a get_absolute_url() method that will allow us to omit all the success_urls and redirect URLs from our generic editing views. Once we've added this, save the file, make new migrations, and run them:

python3 manage.py makemigrations
python3 manage.py migrate

Register the new model

Go ahead and add this model to the main_app/admin.py:

# Add the CatToy model to this import line
from .models import Cat, CatToy

# Register the model below the others
...
admin.site.register(CatToy)

Now we can easily read, create, update, and delete CatToys via the admin interface. Let's now add some basic CRUD routes for it.

CRUD Routes for the New CatToy Model

We do need the ability to create, read, update, and delete each CatToy since it is one of our models. Let's quickly set up URLs, views, and template forms for that just like we did in the CRUD Forms lesson:

URLs

We will need 5 total routes for this new Model: read all, read one, create one, update one, delete one. The URLs will largely follow the same pattern as the ones for Cat. Here are all 5 that we need to add. In the interest of time, just paste these into the urlpatterns list in your main_app/urls.py:

path('cattoys/', views.cattoys_index, name='cattoys_index'),
path('cattoys/<int:cattoy_id>', views.cattoys_detail, name='cattoys_detail'),
path('cattoys/create/', views.CatToyCreate.as_view(), name='cattoys_create'),
path('cattoys/<int:pk>/update/', views.CatToyUpdate.as_view(), name='cattoys_update'),
path('cattoys/<int:pk>/delete/', views.CatToyDelete.as_view(), name='cattoys_delete'),

Views

We can also use the same patterns for our corrsponding view functions as we used for Cat. Don't forget to import your CatToy model, then add these into your main_app/views.py:

# import the CatToy model
from .models import Cat, CatToy

# Add these CatToy Form classes

class CatToyCreate(CreateView):
    model = CatToy
    fields = '__all__'

class CatToyUpdate(UpdateView):
    model = CatToy
    fields = ['name', 'color']

class CatToyDelete(DeleteView):
    model = CatToy
    success_url = '/cattoys'

# Add these CatToy View functions

def cattoys_index(request):
    cattoys = CatToy.objects.all()
    return render(request, 'cattoys/index.html', { 'cattoys': cattoys })

def cattoys_detail(request, cattoy_id):
    cattoy = CatToy.objects.get(id=cattoy_id)
    return render(request, 'cattoys/detail.html', { 'cattoy': cattoy })

Templates

Lastly, we need a few templates for this new model. We need the two forms for our generic editing views and we need an index and a details page. Our two forms will go into main_app/templates/main_app. One will be cattoy_form.html and the other will be cattoy_confirm_delete.html. Recall that this is the naming convention for these form templates when we are using generic editing views. Let's add those now:

<!-- main_app/templates/main_app/cattoy_form.html -->
{% extends 'base.html' %}

{% block content %}
    <form action="" method="post">
        {% csrf_token %}
        <table>
            {{ form.as_table }}
        </table>
        <input type="submit" value="Submit!">
    </form>
{% endblock %}
<!-- main_app/templates/main_app/cattoy_confirm_delete.html -->
{% extends 'base.html' %}

{% block content %}
    <h1>Delete Cat Toy</h1>

    <h4>Are you sure you want to delete <strong>{{ cattoy }}</strong>?</h4>

    <form action="" method="POST">
        {% csrf_token %}
        <input type="submit" value="Yes - Delete!">
        <a href="{% url 'cattoys_detail' cattoy.id %}">Cancel</a>
    </form>
{% endblock %}

Now we'll add the two "read" pages, cattoys\index.html and cattoys\detail.html. We need to make a directory inside our templates directory named cattoys and our pages will go in there:

<!-- templates/cattoys/detail.html -->
{% extends 'base.html' %}
{% block content %}

<h1>Cat Toy Details</h1>

<div class="row">
    <div class="col s6">
        <div class="card">
            <div class="card-content">
                <span class="card-title">{{ cattoy.name }}</span>
                <p>Color: {{ cattoy.color }}</p>
            </div>
            <div class="card-action">
                <a href="{% url 'cattoys_update' cattoy.id %}">Edit</a>
                <a href="{% url 'cattoys_delete' cattoy.id %}">Delete</a>
            </div>
        </div>
    </div>
</div>
{% endblock %}
<!-- templates/cattoys/index.html -->
{% extends 'base.html' %}
{% block content %}

<h1>Cat Toy List</h1>

{% for cattoy in cattoys %}
    <div class="card">
        <a href="{% url 'cattoys_detail' cattoy.id %}">
            <div class="card-content">
                <span class="card-title">{{ cattoy.name }}</span>
                <p>Color: {{ cattoy.color }}</p>
            </div>
        </a>
    </div>
{% endfor %}

{% endblock %}

TEST!

This is a great place to test what we've written. We've just added 5 new routes. We need to test them. Restart your server if it was running and let's test these routes:

  1. Test http://localhost:8000/cattoys/create for creating new cat toys. Add a couple.

  2. Test http://localhost:8000/cattoys for showing the toys you've added.

  3. Click into one of the toys to test the details page.

  4. Test the update link on one of the toys.

  5. Test the delete link for one of the toys.

Adding the Many-to-Many Field to the Model

With that code in place, we can start working on updating our existing Cat Model and pages. The way that we establish a m:m relationship is by adding a ManyToManyField to a model. Django requires only one of our models to have this field and it takes care of the rest. We must only decide which model to put the field into. I think we will mostly be viewing toys by looking at the Cat that owns them so let's add cattoys to the Cat model:

class Cat(models.Model):
    ...
    # Add this line below the other fields
    cattoys = models.ManyToManyField(CatToy)
    ...

That is really all we need to do to set up the relationship. But we have changed a model so we now need to generate some new migrations and run them. Open up a terminal and run these commands from your project folder:

python3 manage.py makemigrations

...then...

python3 manage.py migrate

Now we need to update our CatCreate and CatUpdate view classes to include the new field:

# main_app/views.py
class CatCreate(CreateView):
    model = Cat
    # Update the line below - add 'cattoys'
    fields = ['name', 'breed', 'description', 'age', 'cattoys']

class CatUpdate(UpdateView):
    model = Cat
    # Update the line below - add 'cattoys'
    fields = ['name', 'breed', 'description', 'age', 'cattoys']

Save everything, restart the server if necessary and visit the Cat Create page. Yay! Now we can add multiple cat toys to any of our Cats.

Add cattoys to the Cat detail page

In the style of our detail pages so far, let's add something to show toys. Right above the for block for the photos in cats/detail.html, add this code:

<!-- templates/cats/detail.html -->
<!-- above section for photos -->
...
{% for cattoy in cat.cattoys.all %}
    <div>{{cattoy.name}}, Color: {{cattoy.color}}</div>
{% empty %}
    <div>Cat Has No Toys :(</div>
{% endfor %}
...

Add Cat owners to CatToy detail page

Because this is a many-to-many, it probably makes sense to show all the related Cats that own any particular toy. Let's update the cattoys/detail.html page.

<!-- templates/cattoys/detail.html -->
<!-- above section for photos -->
...
        </div>
        <!-- New Code below here -->
        Owned by:
        {% for cat in cattoy.cat_set.all %}
            <div>{{cat.name}}</div>
        {% empty %}
            <div>Nobody Has This Toy</div>
        {% endfor %}
        <!-- New Code above here -->
    </div>
</div>
{% endblock %}
...

Conclusion

Congratulations! You've completed a substantial full-stack app in Python and Django.

Last updated