encode / django-rest-framework

Web APIs for Django. 🎸
https://www.django-rest-framework.org
Other
28.51k stars 6.85k forks source link

List Serializer Breaks on Unique Constraint Validator #9484

Open max-muoto opened 3 months ago

max-muoto commented 3 months ago

Checklist

3.15 added support for unique constraint validators in serializers: https://github.com/encode/django-rest-framework/pull/7438

This unfortunately breaks when it comes to list serializers. It could make sense these not to run for list serializers if it's difficult to do so in an efficient manner without an O(N) query, and document that fact.

Reproducible Example

from django.db import models
from django.db.models import UniqueConstraint, Q
from rest_framework import serializers

class Pet(models.Model):
    name = models.CharField(max_length=100)
    animal_type = models.CharField(max_length=100)
    can_fly = models.BooleanField(null=True)

    class Meta:
        constraints = [
            UniqueConstraint(
                fields=["name", "animal_type"],
                name="unique_pet",
            )
        ]

class PetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Pet
        fields = ('name', 'animal_type', 'can_fly')

class PetListSerializer(serializers.ListSerializer):
    child = PetSerializer()

    def create(self, validated_data):
        pets = [Pet(**item) for item in validated_data]
        return Pet.objects.bulk_create(pets)

instances = Pet.objects.all()
new_data = [
    {"name": "Polly", "animal_type": "Parrot", "can_fly": True},
    {"name": "Penguin", "animal_type": "Bird", "can_fly": False},
]
serializer = PetListSerializer(instances, data=new_data, many=True)
if serializer.is_valid(raise_exception=True):
    serializer.save()  # This will create all pets in a single query

Running this will yield

AttributeError: 'BaseQuerySet' object has no attribute 'pk'
ParfaitD9 commented 3 months ago

Hi @max-muoto . I'm not sure to understand why you are trying to introduce PetListSerializer. It's DRF job to use ListSerializer once you will use PetSerializer with many=True. Also, if your intent is to create new pets in bulk ops, you haven't to pass instance to PetSerializer. Just data.

Your code could look like

# pet/models.py

from django.db import models
from django.db.models import UniqueConstraint

class Pet(models.Model):
    name = models.CharField(max_length=100)
    animal_type = models.CharField(max_length=100)
    can_fly = models.BooleanField(null=True)

    class Meta:
        constraints = [
            UniqueConstraint(
                fields=["name", "animal_type"],
                name="unique_pet",
            )
        ]

# pet/serializers.py

from rest_framework import serializers
from pet.models import Pet

class PetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Pet
        fields = ("name", "animal_type", "can_fly")

# in your view, you have to write

s = PetSerializer(data=[{"name":"Bob", "animal_type":"mammal", "can_fly":False}, {"name":"Drake", "animal_type": "not mamal"}], many=True)
s.is_valid()
s.save() # pets are created here

# if you try to create same entries again 
s = PetSerializer(data=[{"name":"Bob", "animal_type":"mammal", "can_fly":False}, {"name":"Drake", "animal_type": "not mamal"}], many=True)
s.is_valid() # False
s.errors # [{'non_field_errors': [ErrorDetail(string='The fields name, animal_type must make a unique set.', code='unique')]}, {'non_field_errors': [ErrorDetail(string='The fields name, animal_type must make a unique set.', code='unique')]}]

I hope this helps you.

SorianoMarmol commented 1 month ago

I've a similar issue with 3.15.2, but updating:

        instances = self.get_queryset()
        serializer = self.get_serializer(instances, data=data, partial=False, many=True)
        serializer.is_valid(raise_exception=True)

rest_framework/validators.py", line 153, in exclude_current_instance return queryset.exclude(pk=instance.pk) ^^^^^^^^^^^ AttributeError: 'MyQueryset' object has no attribute 'pk'

It's also a bulk operation but for updates.

the problem could be in my implementation, but it wasn’t failing in version 3.14.

SorianoMarmol commented 1 month ago

I can confirm that if i change my constraint to unique_together instead of UniqueConstraint the issue also happens with 3.14 so in this case could be my implementation