Nathan
Nathan

Reputation: 509

How to create multiple model instances without duplicated writable nested serializer in Django REST Framework?

I have two related models: Product and ProductDescription. In 1 submit action user able to insert a new Product with multiple descriptions depend on the available languages. I use writable nested serializer to insert into Product and ProductDescription simultaneously. I do it by overriding create function in ProductDescriptionSerializer class, it works. However, I can only insert 1 ProductDescription at a time.

Then I tried to use this answer to create multiple model instances at once. The problem is it also creates the same Product twice instead of using the newly created Product Id to insert the next ProductDescription.

My models.py:

class Product(models.Model, ProductStatus):
    product_code = models.CharField(max_length=6)
    color = models.ForeignKey(ColorParent, on_delete=models.SET_NULL, null=True)
    collection = models.ForeignKey(ProductCollection, on_delete=models.SET_NULL, null=True)
    video = models.URLField(verbose_name='Video URL', max_length=250, null=True, blank=True)
    status = models.CharField(max_length=20, choices=ProductStatus.status, default=ProductStatus.active)


class ProductDescription(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    language = models.ForeignKey(Language, on_delete=models.CASCADE)
    description = models.TextField(max_length=500, null=True, blank=True)

    def __str__(self):
        return '%s - %s' % (self.product, self.language)

My serializers.py:

class CustomRelatedField(serializers.RelatedField):
    def display_value(self, instance):
        return instance

    def to_representation(self, value):
        return str(value)

    def to_internal_value(self, data):
        model = self.queryset.model
        return model.objects.get(id=data)


class ProductSerializer(serializers.ModelSerializer):
    collection = CustomRelatedField(queryset=ProductCollection.objects.all(), many=False)
    color = CustomRelatedField(queryset=ColorParent.objects.all(), many=False)

    class Meta:
        model = Product
        fields = ['id', 'product_code', 'collection', 'color', 'video', 'status']


class ProductDescriptionSerializer(serializers.ModelSerializer):
    product = ProductSerializer()
    language = CustomRelatedField(many=False, queryset=Language.objects.all())

    class Meta:
        model = ProductDescription
        fields = ['id', 'product', 'language', 'description']

    def to_representation(self, instance):
        data = super().to_representation(instance)
        if self.context['request'].method == 'GET':
            data['product'] = instance.product.product_code
            return data
        return Serializer.to_representation(self, instance)

    # The `.create()` method does not support writable nested fields by default.
    def create(self, validated_data):
        # create product data for Product model.
        product_data = validated_data.pop('product')
        product = Product.objects.create(**product_data)

        # create ProductDescription and set product FK.
        product_description = ProductDescription.objects.create(product=product, **validated_data)

        # return ProductDescription instance.
        return product_description

My views.py:

class CreateListModelMixin(object):
    def get_serializer(self, *args, **kwargs):
        if isinstance(kwargs.get('data', {}), list):
            kwargs['many'] = True
        return super(CreateListModelMixin, self).get_serializer(*args, **kwargs)


class ProductDescriptionView(CreateListModelMixin, viewsets.ModelViewSet):
    permission_classes = [permissions.DjangoModelPermissions]
    queryset = ProductDescription.objects.all()
    serializer_class = ProductDescriptionSerializer
    http_method_names = ['get', 'head', 'post', 'put', 'patch', 'delete']

The JSON format I use to POST data:

[
  {
    "product": {
        "product_code": "BQ1080",
        "collection": 5,
        "color": 7,
        "video": "https://www.youtube.com/watch?v=",
        "status": "Continue"
    },
    "language": 1,
    "description": "English description."
  },
  {
    "product": {
        "product_code": "BQ1080",
        "collection": 5,
        "color": 7,
        "video": "https://www.youtube.com/watch?v=",
        "status": "Continue"
    },
    "language": 2,
    "description": "Vietnamese description."
  }
]

It creates a duplicate Product in Product List:

[
    {
        "id": 26,
        "product_code": "BQ1080",
        "collection": 5,
        "color": 7,
        "video": "https://www.youtube.com/watch?v=",
        "status": "Continue"
    },
    {
        "id": 27,
        "product_code": "BQ1080",
        "collection": 5,
        "color": 7,
        "video": "https://www.youtube.com/watch?v=",
        "status": "Continue"
    }
]

The ProductDescription datas are correct though:

[
    {
        "id": 5,
        "product": "BQ1080",
        "language": "English",
        "description": "English description."
    },
    {
        "id": 6,
        "product": "BQ1080",
        "language": "Vietnam",
        "description": "Vietnamese description."
    }
]

Upvotes: 1

Views: 1191

Answers (3)

neverwalkaloner
neverwalkaloner

Reputation: 47374

To avoid duplicate product you can use get_or_create() method:

class ProductDescriptionSerializer(serializers.ModelSerializer):

    ...

    def create(self, validated_data):
        # create product data for Product model.
        product_data = validated_data.pop('product')
        product_code = product_data.pop("product_code") 
        product, _ = Product.objects.get_or_create(product_code=product_code, defaults=product_data)

        # create ProductDescription and set product FK.
        product_description = ProductDescription.objects.create(product=product, **validated_data)

        # return ProductDescription instance.
        return product_description

Note that get_or_create is prone to race condition. So if two same requests came to you service at the same time you may still have duplicate products.

Upvotes: 1

ruddra
ruddra

Reputation: 52028

I think you need to override your ProductSerializer's create method. Maybe you can try like this:

class ProductSerializer(serializers.ModelSerializer):
    collection = CustomRelatedField(queryset=ProductCollection.objects.all(), many=False)
    color = CustomRelatedField(queryset=ColorParent.objects.all(), many=False)

    def create(self, validated_data):
        instance, _ = Product.objects.get_or_create(**validated_data)
        return instance

    class Meta:
        model = Product
        fields = ['id', 'product_code', 'collection', 'color', 'video', 'status']

So that, first it will try to get if the Product exists, else create the instance(hence reducing duplicate entry).

Upvotes: 1

Cem
Cem

Reputation: 502

ForeignKey not for this job you should use ManyToManyField

Upvotes: 0

Related Questions