48

I'm having a problem of updating a nested object.

So I have a model which structure is similar to this one:

class Invoice(models.Model):
    nr = models.CharField(max_length=100)
    title = models.CharField(max_length=100)

class InvoiceItem(models.Model):
    name = models.CharField(max_length=100)
    price = models.FloatField()
    invoice = models.ForeignKey(Invoice, related_name='items')

I need to create child objects from parent, and what I mean by that, is to create InvoiceItems directly when creating an Invoice object. For this purpose, I've wrote the following serializers:

class InvoiceItemSerializer(serializers.ModelSerializer):
    invoice = serializers.PrimaryKeyRelatedField(queryset=Invoice.objects.all(), required=False)
    class Meta:
        model = InvoiceItem


class InvoiceSerializer(serializers.ModelSerializer):
    items = InvoiceItemSerializer(many=True)

    class Meta:
        model = Invoice

    def create(self, validated_data):
        items = validated_data.pop('items', None)
        invoice = Invoice(**validated_data)
        invoice.save()
        for item in items:
            InvoiceItem.objects.create(invoice=invoice, **item)
        return invoice

Up till now, the create/read/delete methods work perfectly, except the update. I think the below logic should be correct, but it misses something.

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

    # up till here everything is updating, however the problem appears here.
    # I don't know how to get the right InvoiceItem object, because in the validated
    # data I get the items queryset, but without an id.

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

    return instance

Any help would be really appreciated.

6
  • 1
    You are probably going to have to pass the pk as part of the payload. Commented May 15, 2016 at 16:29
  • I am passing, but it is not present in validated_data Commented May 15, 2016 at 16:33
  • Can you show your payload? Commented May 15, 2016 at 16:57
  • here is how i'm making the request pastie.org/private/3mnrcxp64ra4j65kvcmoyw Commented May 15, 2016 at 17:21
  • How to solve the same problem in Django rest framework 3 because Creating a ModelSerializer without either the 'fields' attribute or the 'exclude' attribute has been deprecated since 3.3.0. Commented Mar 23, 2017 at 1:53

9 Answers 9

57

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.

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for the sample. But users please do not forgot to catch DoesNotExist exception which inv_item = InvoiceItem.objects.get(id=item_id, invoice=instance) may raise.
I am not getting the id field in validated data object in the update function. What did I do wrong?
It's worth noting that if you're creating a true "id" field, you may want to follow django's convention of: id = models.BigAutoField(primary_key=True) (see docs.djangoproject.com/en/3.2/topics/db/models/…) Also, instead of checking if InvoiceItem exists and splitting up your code, you can just use the helper get_or_create: InvoiceItem.objects.get_or_create() docs.djangoproject.com/en/3.2/ref/models/querysets/…
22

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.

3 Comments

Downvoters: Please offer feedback or reasoning for your downvote so we can continue to improve the content on StackOverflow. Thank you
Good solution but what if the userprofile is deleted and you are checking if it is not in validated data then do nothing?
If the userprofile doesn't exist, this code should still work. This is just my opinion, but I'm not sure it's a good idea to allow users to delete their linked userprofile.
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

1 Comment

and what happens in the case of a create method where id is redundant?
10

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()

2 Comments

Looks like this is the correct answer. It covers removing items as well.
@KetanShukla What's the difference doing this CRUD operation in views instead of serializers ?
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()

Comments

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

Comments

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

Comments

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    

Comments

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.