1

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."
    }
]
halfer
  • 19,824
  • 17
  • 99
  • 186
Nathan
  • 449
  • 1
  • 7
  • 23

3 Answers3

1

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

ruddra
  • 50,746
  • 7
  • 78
  • 101
  • Thanks for answering my question, ruddra. The `Product` instance still inserted twice. Should I add/change another part of my code as well? – Nathan Feb 07 '20 at 10:57
1

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.

neverwalkaloner
  • 46,181
  • 7
  • 92
  • 100
  • I got `"non_field_errors": ["Invalid data. Expected a dictionary, but got list."]` when I used the same JSON format as in my post. How do I post the new data format? – Nathan Feb 07 '20 at 10:24
  • 1
    @Nathan it strange, did you changed something else? You should still use solution with many=True. Do not remove it. – neverwalkaloner Feb 07 '20 at 10:34
  • 1
    Sorry. I did mistakenly change something. It works now. Thank you neverwalkaloner! – Nathan Feb 07 '20 at 10:46
0

ForeignKey not for this job you should use ManyToManyField

Cem
  • 428
  • 4
  • 14