Updating A Many-To-Many Relationship In Django
Tags: django, drf, tutorial
Published: 2022-02-24 16:22:37

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.

```python
# 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.

```python
# 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
>>> 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`.

```python
# 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.

```python
serializer = PostSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
```

Once the data is set and valid, we save the post.

```python
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.

```python
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.

```python
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`.

```python
# 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.

```python
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.

```python
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.

```python
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.

```python
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.

```python
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.

```python
# 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.

```python
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.

```python
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](https://github.com//danielcoker/blogmany). View the API documentation for sample requests [here](https://documenter.getpostman.com/view/6500233/UVkpMv6t).