Wouter Goossens
Wouter Goossens

Reputation: 121

Django Rest Framework - Create and Update Parent-Child relation

I'm trying to use the Django Rest Framework serializers to make API's for my front-end to create / update and delete articles in a store. These articles can have multiple prices (depending on time). So there is a one-to-many relation from article (one) to price (many). I have defined this in models.py:

class Article(models.Model):
    article_id = models.AutoField(primary_key=True, verbose_name="Article ID")
    article_name = models.CharField(max_length=250, verbose_name="Name")
    article_order = models.IntegerField(verbose_name="Order")

class Price(models.Model):
    price_id = models.AutoField(primary_key=True, verbose_name="Price ID")
    article_id = models.ForeignKey(Article, on_delete=models.CASCADE, verbose_name="Article ID", related_name="Prices")
    price_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="Price")

My serializers.py file looks like this:

from rest_framework import serializers
from .models import *

class PriceSerializer(serializers.ModelSerializer):

    class Meta:
        model = Price
        fields = ('price_price',)

class ArticleSerializer(serializers.ModelSerializer):

    Prices = PriceSerializer(many=True)

    def create(self, validated_data):
        prices_data = validated_data.pop('Prices')
        article = Article.objects.create(**validated_data)
        for price_data in prices_data:
            Price.objects.create(article_id=article, **price_data)
        return article

    def update(self, instance, validated_data):
        prices_data = validated_data.pop('Prices')
        Article.objects.filter(article_id=instance.article_id).update(**validated_data)
        for price_data in prices_data:
            Price.objects.get_or_create(article_id=instance, **price_data)
        return instance

    class Meta:
        model = Article
        fields = '__all__'

This works perfectly and I can create a new article-price measurement with this data: (article_order will be used later for ordering the list)

{"Prices":[{"price_price":"1"}],"article_name":"Article A","article_order":"1"}

Until this point, everything is working as expected. But when I try to update the prices, the Price.objects.get_or_create() statement does not recognize existing prices. For example: when the first price is €10, then €20 and then again €10, the last price will not be inserted because the get_or_create statement does not recognize this as a new instance of the price model. That probably makes sense, because the PriceSerializer does not serialize the price_id. But when I change the serializer to:

class PriceSerializer(serializers.ModelSerializer):

    class Meta:
        model = Price
        fields = '__all__

, I can no longer create instances because the ArticleSerializer is requiring an article_id for the Price instance (but that doesn't exist yet).

Does anyone have an idea how to solve this problem? The DRF documentation only includes the create statement for this type of nested serializer.

https://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers

Upvotes: 6

Views: 4884

Answers (2)

qinglin xiao
qinglin xiao

Reputation: 1

You can see from Django REST Framework documentation here about serializers.

 def update(self, instance, validated_data):
     instance.email = validated_data.get('email', instance.email)
     instance.content = validated_data.get('content', instance.content)
     instance.created = validated_data.get('created', instance.created)
     return instance

I hope it can help you.

Upvotes: 0

Ken4scholars
Ken4scholars

Reputation: 6296

You're not using the get_or_create method correctly. It accepts kwargs which are used to lookup the object and if it does not exist, creates them, adding other field values from defaults.

This means that if you pass the new values to update as kwargs, it won't find such object because it currently has different values for those fields. Instead, you should pass in fields that do not change in kwargs and the rest in defaults.

This is what you should do

def update(self, instance, validated_data):
    prices_data = validated_data.pop('Prices')
    Article.objects.filter(article_id=instance.article_id).update(**validated_data)
    for price_data in prices_data:
        Price.objects.get_or_create(article_id=instance, defaults=price_data)
    return instance

Note however, that you may get a MutipleObjectsReturned error since an article can have several prices based on your DB architecture. You may want to change the article_id to a OneToOneField to prevent this if mutiple prices per Article was not intended.

You can read more about get_or_create in the Django docs

As a side note, in OOP architecture id in Article already implies article id, so prepending article to your field names is a really redundant and bad practice. And again, you may want to use the Django autogenerated id field instead of redefining it, unless you want to customize it in some way, which is currently not obvious in your code

Upvotes: 2

Related Questions