MalikWahab
Github

How to bulk create and update with Django REST Framework

July 02, 2020

Introduction

Django REST Framework provides a way to bulk create and update objects with a single request. This is useful when you want to create or update a large number of objects. In this post, we’ll see how to bulk create and update objects with Django REST Framework.

Before we start, let’s take a look at some code snippets from a simple Django REST Framework project.

models.py

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=100)

serializers.py

from rest_framework import serializers

from .models import Person

class PersonSerializer(serializers.ModelSerializer):
    class Meta:
        model = Person
        fields = ('id', 'name')

views.py

from rest_framework import viewsets

from .models import Person
from .serializers import PersonSerializer

class PersonViewSet(viewsets.ModelViewSet):
    queryset = Person.objects.all()
    serializer_class = PersonSerializer

urls.py

from django.conf.urls import url, include
from rest_framework import routers

from .views import PersonViewSet

router = routers.DefaultRouter()
router.register(r'people', PersonViewSet)

urlpatterns = [
    url(r'^', include(router.urls)),
]

tests.py

from django.test import TestCase
from django.urls import reverse

from rest_framework import status
from rest_framework.test import APITestCase

from .models import Person

class PersonTests(APITestCase):
    def test_create_person(self):
        """
        Ensure we can create a new person object.
        """
        url = reverse('person-list')
        data = {'name': 'John'}
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Person.objects.count(), 1)
        self.assertEqual(Person.objects.get().name, 'John')

    def test_update_person(self):
        """
        Ensure we can update an existing person object.
        """
        person = Person.objects.create(name='John')
        url = reverse('person-detail', kwargs={'pk': person.pk})
        data = {'name': 'Jane'}
        response = self.client.put(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(Person.objects.count(), 1)
        self.assertEqual(Person.objects.get().name, 'Jane')

This code allows us to create and update a single Person object with a single request. Let’s see how we can bulk create and update objects with Django REST Framework.

Bulk create and update with Django REST Framework

Django REST Framework provides a way to bulk create and update objects with a single request. This is useful when you want to create or update a large number of objects. The trick is to provide a list_serializer_class attribute in the Meta class of the serializer. The list_serializer_class attribute should be set to a class that inherits from the ListSerializer class. In the ListSerializer class, we can override the create and update methods to bulk create and update objects.

Before we implement the serializer, let’s update our tests to check if we can bulk create and update objects.


class PersonTests(APITestCase):
    def test_create_person(self):
        """
        Ensure we can create a new person object.
        """
        url = reverse('person-list')
        data = {'name': 'John'}
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Person.objects.count(), 1)
        self.assertEqual(Person.objects.get().name, 'John')

    def test_create_people(self):
        """
        Ensure we can bulk create new person objects.
        """
        url = reverse('person-list')
        data = [
            {'id': 1, 'name': 'John'},
            {'id': 2, 'name': 'Jane'},
            {'id': 3, 'name': 'Jack'},
        ]
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Person.objects.count(), 2)\
        # check that Person object with name 'John' exists
        self.assertTrue(Person.objects.filter(name='John').exists())
        # check that Person object with name 'Jane' exists
        self.assertTrue(Person.objects.filter(name='Jane').exists())
        # check that Person object with name 'Jack' does not exist
        self.assertFalse(Person.objects.filter(name='Jack').exists())

    def test_update_person(self):
        """
        Ensure we can update an existing person object.
        """
        person = Person.objects.create(name='John')
        url = reverse('person-detail', kwargs={'pk': person.pk})
        data = {'name': 'Jane'}
        response = self.client.put(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(Person.objects.count(), 1)
        self.assertEqual(Person.objects.get().name, 'Jane')

    def test_update_people(self):
        """
        Ensure we can bulk update existing person objects.
        """
        john = Person.objects.create(name='John')
        jane = Person.objects.create(name='Jane')
        url = reverse('person-list')
        data = [{'id': 1, 'name': 'John Doe'}, {'id': 2, 'name': 'Jane Doe'}]
        response = self.client.put(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(Person.objects.count(), 2)
        self.assertEqual(Person.objects.get(pk=john.pk).name, 'John Doe')
        self.assertEqual(Person.objects.get(pk=jane.pk).name, 'Jane Doe')

This should fail because we haven’t implemented the ListSerializer class yet. Let’s implement the ListSerializer class.

Serializers

Let’s edit the serializers.py file to implement the ListSerializer class, PersonListSerializer and pass it to the list_serializer_class attribute in the Meta class of the PersonSerializer class.

serializers.py

from rest_framework import serializers

from .models import Person

class PersonSerializer(serializers.ModelSerializer):
    class Meta:
        model = Person
        fields = ('id', 'name')
        list_serializer_class = PersonListSerializer

class PersonListSerializer(serializers.ListSerializer):
    def create(self, validated_data):
        people = [Person(**item) for item in validated_data]
        return Person.objects.bulk_create(people)

    def update(self, instance, validated_data):
        mapping = {person.id: person for person in instance}
        data_mapping = {item['id']: item for item in validated_data}

        people = []
        for person_id, data in data_mapping.items():
            person = mapping.get(person_id, None)
            if person:
                people.append(self.child.update(person, data))

        return people

In the create method, we pass the validated data to the Person model and create a list of Person objects. Then, we use the bulk_create method to create the objects in the database. In the update method, we create a mapping of the Person objects and the validated data. Then, we loop through the validated data and update the Person objects.

No changes are required in the views and urls. The views and urls remain the same as before.

views.py

from rest_framework import viewsets

from .models import Person
from .serializers import PersonSerializer

class PersonViewSet(viewsets.ModelViewSet):
    queryset = Person.objects.all()
    serializer_class = PersonSerializer

urls.py

from django.urls import path, include

from .views import PersonViewSet

router = DefaultRouter()
router.register(r'people', PersonViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

Conclusion

In this post, we saw how we could bulk create and update objects with Django REST Framework. By using the list_serializer_class attribute, we can override the create and update methods to bulk create and update objects. I hope you found this post useful. Thanks for reading!


Copyright © 2022 All rights reserved.