Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

List Serializer Breaks on Unique Constraint Validator #9484

Open
max-muoto opened this issue Jul 31, 2024 · 3 comments
Open

List Serializer Breaks on Unique Constraint Validator #9484

max-muoto opened this issue Jul 31, 2024 · 3 comments

Comments

@max-muoto
Copy link
Contributor

max-muoto commented Jul 31, 2024

Checklist

3.15 added support for unique constraint validators in serializers: #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
Copy link

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
Copy link

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.

  • self.many = True
  • self.instance contains 'MyQueryset'
  • self.initial_data - data to update
  • model has models.UniqueConstraint with multiple fields

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

@SorianoMarmol
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants