Reputation: 3815
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.
Upvotes: 48
Views: 38683
Reputation: 61
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
Upvotes: 1
Reputation: 186
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
Upvotes: 0
Reputation: 2717
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.
Upvotes: 22
Reputation: 113
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()
Upvotes: 9
Reputation: 491
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
Upvotes: 0
Reputation: 325
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()
Upvotes: 4
Reputation: 3815
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.
Upvotes: 57
Reputation: 1061
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
Upvotes: 0
Reputation: 15286
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
Upvotes: 10