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 theadd
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 theset
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—TheTag
and thePost
models. TheTag
model only contains the tag's title. While thePost
model contains information about the post. TheTag
and thePost
have a many-to-many relationship.
# blog/models.pyfromdjango.dbimportmodelsclassTag(models.Model):title=models.CharField(max_length=150)def__str__(self):returnself.titleclassPost(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):returnself.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.pyfromrest_frameworkimportserializersfrom.modelsimportPost,TagclassTagSerializer(serializers.ModelSerializer):classMeta:model=Tagfields=('id','title',)classPostSerializer(serializers.ModelSerializer):tags=TagSerializer(many=True,read_only=True)classMeta:model=Postfields=('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:
>>>pythonmanage.pyshell>>>fromblog.modelsimportTag>>>Tag.objects.create(title='Django')>>>Tag.objects.create(title='Database')
To make a post with tags, we must first create a new view function calledcreate_post
.
# blog/views.py@api_view(['POST'])defcreate_post(request):...
We're making a post with tags in this view function. We begin by passing the request data to thePostSerializer
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 theadd
method. We raise aNotFound
exception if a tag does not exist in the database.
fortag_idinrequest.data.get('tags'):try:tag=Tag.objects.get(id=tag_id)post.tags.add(tag)exceptTag.DoesNotExist:raiseNotFound()
After adding the tags to the post in the above snippet, we will return to the client the response containing the newly created post.
returnResponse(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 calledupdate_post
.
# blog/views.py@api_view(['PUT'])defupdate_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 aNotFound
exception.
try:post=Post.objects.get(id=pk)exceptPost.DoesNotExist:raiseNotFound()
The post and request data are then passed to thePostSerializer
, 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 theset
method to sync the relationship. However, theset
method accepts a list of objects. As a result, we must first generate a list of tag objects and then pass it to theset
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 aNotFound
exception if a tag does not exist in the database.
tags=[]fortag_idinrequest.data.get('tags'):try:tag=Tag.objects.get(id=tag_id)tags.append(tag)except:raiseNotFound()
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 theset
method to sync the posts and tags. We can send the client the response containing the updated post.
returnResponse(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.pyimportjsonfromdjango.urlsimportreversefromrest_frameworkimportstatusfromrest_framework.testimportAPITestCasefrom.modelsimportPost,TagclassPostTest(APITestCase):defsetUp(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.
deftest_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.
deftest_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)
Runpython 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 repohere. View the API documentation for sample requestshere.
Top comments(1)

Good article 👍
For further actions, you may consider blocking this person and/orreporting abuse