Daniel Coker
Posted on February 24, 2022
There is this feeling you get when you're stuck on a problem and you search Google for an answer but don't find it. It’s even more frustrating when it is a seemingly common problem. That was me recently when I was attempting to update a many-to-many relationship in Django
When developing a Django app, many-to-many relationship use cases will arise at various points. Blog posts and tags, books and authors, and so on are examples of many-to-many relationships. A many-to-many relationship is one where multiple records in one table can be related to multiple records of another table. A book, for example, can have multiple authors, and an author can write multiple books.
The relationship between blog posts and tags will be used in this article. Consider a blog app with multiple posts. Multiple tags can be attached to a post, and a tag can belong to multiple posts. Adding tags to the post was simple for me; I simply needed to use the add
method. Where I struggled was in keeping their relationship up to date. I'd like to be able to remove any tags associated with the post that isn't in the list of tags sent with the update post request and then create new associations with new tags in the list of tags.
There are several approaches that can be taken to accomplish this. One approach is to delete all of the tags associated with the post and then re-add them. However, in this article, we'll use the set
method to update their relationship.
To follow this article, I'm assuming you have a Django project with a Django Rest Framework installed. Let's get started.
Setting Up Models and Migrations
To begin, we'll need to create the two models that will be required for this project—The Tag
and the Post
models. The Tag
model only contains the tag's title. While the Post
model contains information about the post. The Tag
and the Post
have a many-to-many relationship.
# blog/models.py
from django.db import models
class Tag(models.Model):
title = models.CharField(max_length=150)
def __str__(self):
return self.title
class Post(models.Model):
title = models.CharField(max_length=150)
body = models.TextField()
tags = models.ManyToManyField(Tag)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
Run the following commands to create and run the migrations.
>>> python manage.py makemigrations
>>> python manage.py migrate
Adding Serializers
We'll add serializers for the models we've created.
# blog/serializers.py
from rest_framework import serializers
from .models import Post, Tag
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ('id', 'title',)
class PostSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ('id', 'title', 'body', 'tags', 'created_at', 'updated_at',)
Creating A Post With Tags
Before we go on to add the view function to create a post, we will need to add tags to the database using the shell. Run:
>>> python manage.py shell
>>> from blog.models import Tag
>>> Tag.objects.create(title='Django')
>>> Tag.objects.create (title='Database')
To make a post with tags, we must first create a new view function called create_post
.
# blog/views.py
@api_view(['POST'])
def create_post(request):
...
We're making a post with tags in this view function. We begin by passing the request data to the PostSerializer
and checking to see if it is valid.
serializer = PostSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
Once the data is set and valid, we save the post.
post = serializer.save()
The following step is to include the tags in the post. Before they can be related in a many-to-many relationship, both records must exist in the database. That is why we had to first create the tags and post.
By looping through the tag ids in the request data, we add the tag to the post. To avoid errors, we check to see if the tag already exists in the database, and then we add the tag to the post using the add
method. We raise a NotFound
exception if a tag does not exist in the database.
for tag_id in request.data.get('tags'):
try:
tag = Tag.objects.get(id=tag_id)
post.tags.add(tag)
except Tag.DoesNotExist:
raise NotFound()
After adding the tags to the post in the above snippet, we will return to the client the response containing the newly created post.
return Response(data=serializer.data, status=status.HTTP_201_CREATED)
Updating A Post With Tags
This is the primary purpose of this article—updating the many to many relationship between the post and tags. To update the post along with the tags, we need to add another view function called update_post
.
# blog/views.py
@api_view(['PUT'])
def update_post(request, pk=None):
...
In this view function, we begin by retrieving the post using the primary key. If the post does not exist, we raise a NotFound
exception.
try:
post = Post.objects.get(id=pk)
except Post.DoesNotExist:
raise NotFound()
The post and request data are then passed to the PostSerializer
, and the request data is validated. We save the post to the database if it is valid.
serializer = PostSerializer(post, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
We'll need to use the set
method to sync the relationship. However, the set
method accepts a list of objects. As a result, we must first generate a list of tag objects and then pass it to the set
method. To generate this list of objects, we'll loop through the tag ids in the request data, just like we did when we made the post. The tag is then checked to see if it exists in the database (to avoid any errors). If the tag already exists, we add it to the tags list. We raise a NotFound
exception if a tag does not exist in the database.
tags = []
for tag_id in request.data.get('tags'):
try:
tag = Tag.objects.get(id=tag_id)
tags.append(tag)
except:
raise NotFound()
We can now set the posts after we've finished creating the list of tags. The set method deletes the association for tags that are no longer in the list and creates new associations for tags that are added to the list.
post.tags.set(tags)
After using the set
method to sync the posts and tags. We can send the client the response containing the updated post.
return Response(data=serializer.data, status=status.HTTP_200_OK)
Add Tests
Let's add some test cases to ensure that everything works as expected. First, include the required imports, define the test class, and add a setup to create three tags for us when each test case is run.
# blog/tests.py
import json
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from .models import Post, Tag
class PostTest(APITestCase):
def setUp(self):
self.tag1 = Tag.objects.create(title='Django')
self.tag2 = Tag.objects.create(title='Database')
self.tag3 = Tag.objects.create(title='Relationship')
This test case asserts that the create post endpoint is able to create a new post.
def test_can_create_post(self):
url = reverse('posts-create')
data = {
'title': 'Test Post',
'body': 'The body of the test post.',
'tags': [self.tag1.id, self.tag2.id],
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
This test case ensures that the update post endpoint updates the post details while also establishing the proper relationship between the tags and the updated post.
def test_can_update_post(self):
post = Post.objects.create(
title='Test Post', body='The body of the test post.')
post.tags.set([self.tag1, self.tag3])
url = reverse('posts-update', kwargs={'pk': post.id})
data = {
'title': 'Updated Test Post',
'body': 'The body of the updated test post.',
'tags': [self.tag2.id, self.tag3.id],
}
response = self.client.put(url, data, format='json')
response_data = json.loads(response.content)
post = Post.objects.get(id=post.id)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response_data['title'], post.title)
self.assertEqual(response_data['body'], post.body)
self.assertEqual(len(response_data['tags']), 2)
self.assertEqual(response_data['tags'][0]['id'], self.tag2.id)
self.assertEqual(response_data['tags'][1]['id'], self.tag3.id)
Run python manage.py test
to execute tests.
Conclusion
In this article, we discussed what a many-to-many relationship is and how to update a many-to-many relationship using Django. We also added tests to ensure that the implementation works properly. The complete code is available in the GitHub repo here. View the API documentation for sample requests here.
Posted on February 24, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.