[Django]-Django-Rest-Framework. Updating nested object

54๐Ÿ‘

โœ…

This is the way Iโ€™ve accomplished the task:

Iโ€™ve added an id field to the InvoiceItemSerializer

class InvoiceItemSerializer(serializers.ModelSerializer):
    ...
    id = serializers.IntegerField(required=False)
    ...

And the update method for the InvoiceSerializer

def update(self, instance, validated_data):
    instance.nr = validated_data.get('nr', instance.nr)
    instance.title = validated_data.get('title', instance.title)
    instance.save()

    items = validated_data.get('items')

    for item in items:
        item_id = item.get('id', None)
        if item_id:
            inv_item = InvoiceItem.objects.get(id=item_id, invoice=instance)
            inv_item.name = item.get('name', inv_item.name)
            inv_item.price = item.get('price', inv_item.price)
            inv_item.save()
        else:
            InvoiceItem.objects.create(account=instance, **item)

    return instance

Also in the create method Iโ€™m popping the id if it is passed.

๐Ÿ‘คdimmg

21๐Ÿ‘

All of these solutions seemed too complex or too specific for me, I ended up using code from the tutorial here which was incredibly simple and reusable:

from rest_framework import serializers
from django.contrib.auth import get_user_model
from myapp.models import UserProfile


# You should already have this somewhere
class UserProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserProfile
        fields = ['nested', 'fields', 'you', 'can', 'edit']


class UserSerializer(serializers.ModelSerializer):
    # CHANGE "userprofile" here to match your one-to-one field name
    userprofile = UserProfileSerializer()

    def update(self, instance, validated_data):
        # CHANGE "userprofile" here to match your one-to-one field name
        if 'userprofile' in validated_data:
            nested_serializer = self.fields['userprofile']
            nested_instance = instance.userprofile
            nested_data = validated_data.pop('userprofile')

            # Runs the update on whatever serializer the nested data belongs to
            nested_serializer.update(nested_instance, nested_data)

        # Runs the original parent update(), since the nested fields were
        # "popped" out of the data
        return super(UserSerializer, self).update(instance, validated_data)

EDIT: Bugfix, I added a check for the nested fieldโ€™s existence before attempting to update it.

๐Ÿ‘คPreston Badeer

10๐Ÿ‘

I came across the same problem recently. The way I addressed it was to force the id to be a required field:

class MySerializer(serializers.ModelSerializer):

    class Meta:
        model = MyModel
        fields = ('id', 'name', 'url', )
        extra_kwargs = {'id': {'read_only': False, 'required': True}}

This way I was able to retrieve the correct instance and update it

๐Ÿ‘คdjq

7๐Ÿ‘

I think Vitor Hugo Moralesโ€™s answer is great and would like to contribute one cent of mine by looping through the keys to assign each field in the object to that in validated data rather than hard-coding it the way he has done. For example,

def update_product_items(self, instance, validated_data):
    # get the nested objects list
    product_items = validated_data.pop('products')
    # get all nested objects related with this instance and make a dict(id, object)
    product_items_dict = dict((i.id, i) for i in instance.products.all())

    for item_data in product_items:
        if 'id' in item_data:
            # if exists id remove from the dict and update
            product_item = product_items_dict.pop(item_data['id'])
            # remove id from validated data as we don't require it.
            item_data.pop('id')
            # loop through the rest of keys in validated data to assign it to its respective field
            for key in item_data.keys():
                setattr(product_item,key,item_data[key])

            product_item.save()
        else:
            # else create a new object
            ProductItem.objects.create(product=instance, **item_data)

    # delete remaining elements because they're not present in my update call
    if len(product_items_dict) > 0:
        for item in product_items_dict.values():
            item.delete()
๐Ÿ‘คKetan Shukla

4๐Ÿ‘

In my case I wish to update all list of nested objects even if theyโ€™re deleted.

I donโ€™t want to in every nested object delete, call the nested Model DELETE method; just update entire object and your nested object list.

For this implementation: 1-Product has N-ProductItems

def update_product_items(self, instance, validated_data):
    # get the nested objects list
    product_items = validated_data.pop('products')
    # get all nested objects related with this instance and make a dict(id, object)
    product_items_dict = dict((i.id, i) for i in instance.products.all())

    for item_data in product_items:
        if 'id' in item_data:
            # if exists id remove from the dict and update
            product_item = product_items_dict.pop(item_data['id'])

            product_item.quantity = item_data['quantity']
            product_item.size_pmg = item_data['size_pmg']
            product_item.size_number = item_data['size_number']
            product_item.color = item_data['color']
            product_item.save()
        else:
            # else create a new object
            ProductItem.objects.create(product=instance, **item_data)

    # delete remaining elements because they're not present in my update call
    if len(product_items_dict) > 0:
        for item in product_items_dict.values():
            item.delete()

1๐Ÿ‘

I would add transaction and use some util django methods

from django.db import transaction

def update(self, instance, validated_data):
    with transaction.atomic():
        items = validated_data.pop('items', None)
        for key, value in validated_data.items():
            setattr(instance, key, value)
        instance.save()

        for item in items:
            inv_item, created = InvoiceItem.objects.update_or_create(id=item['id'], invoice=instance, defaults={**item})

        return instance
๐Ÿ‘คlautaro dapin

0๐Ÿ‘

Try

def update(self, instance, validated_data):
    instance.nr = validated_data.get('nr', instance.nr)
    instance.title = validated_data.get('title', instance.title)
    instance.save()


    items = validated_data.get('items')
    for item in items:
        inv_item = InvoiceItem.objects.get(invoice=instance, pk=item.pk)
        inv_item.name = item.get('name', inv_item.name)
        inv_item.price = item.get('price', inv_item.price)
        inv_item.invoice = instance
        inv_item.save()

    instance.save()
    return instance
๐Ÿ‘คFACode

0๐Ÿ‘

Try this.

from rest_framework.utils import model_meta

class InvoiceSerializer(serializers.ModelSerializer):
    invoice_item=InvoiceItemSerializer(many=True,required=False)

    field_map={"invoice_item" : { "model":  models.InvoiceItem
                                   "pk_field" : "id"}}    



    class Meta:
        model = models.Invoice
        fields = '__all__'

    def create(self, validated_data):
        extra_data={}
        for key in self.field_map.keys():
            extra_data[key]=validated_data.pop(key,[])

        # create invoice
        invoice = models.Invoice.objects.create(**validated_data)

        for key in extra_data.keys():
            for data in extra_data[key]:
                self.field_map[key]["model"].objects.create(invoice=invoice,**data)

        return invoice

    def _update(self,instance,validated_data):
        #drf default implementation
        info = model_meta.get_field_info(instance)

        for attr, value in validated_data.items():
            if attr in info.relations and info.relations[attr].to_many:
                field = getattr(instance, attr)
                field.set(value)
            else:
                setattr(instance, attr, value)
        instance.save()
        return instance

    def update(self,instance,validated_data):

        extra_data={}
        for key in self.field_map.keys():
            extra_data[key]=validated_data.pop(key,[])

        instance=self._update(instance,validated_data)

        for key in extra_data.keys():
            for data in extra_data[key]:

                id=data.get(self.field_map[key]["pk_field"],None)
                if id:
                    try:
                        related_instance=self.field_map[key]["model"].objects.get(id=id)
                    except:
                        raise
                    self._update(related_instance,data)
                else:
                    self.field_map[key]["model"].objects.create(**data)

        return instance    
๐Ÿ‘คCuriousGeorge

0๐Ÿ‘

The drf-writable-nested package provides writable nested model serializer which allows to create/update models with nested related data.

https://github.com/beda-software/drf-writable-nested

๐Ÿ‘คDr Sheldon

Leave a comment