问题
I'm using Python 3.7, Django 2.2, the Django rest framework, and pytest. I have the following model, in which I want to re-use an existing model if it exists by its unique key ...
class CoopTypeManager(models.Manager):
def get_by_natural_key(self, name):
return self.get_or_create(name=name)[0]
class CoopType(models.Model):
name = models.CharField(max_length=200, null=False, unique=True)
objects = CoopTypeManager()
Then I have created the below serializer to generate this model from REST data
class CoopTypeSerializer(serializers.ModelSerializer):
class Meta:
model = CoopType
fields = ['id', 'name']
def create(self, validated_data):
"""
Create and return a new `CoopType` instance, given the validated data.
"""
return CoopType.objects.get_or_create(**validated_data)
def update(self, instance, validated_data):
"""
Update and return an existing `CoopType` instance, given the validated data.
"""
instance.name = validated_data.get('name', instance.name)
instance.save()
return instance
However, when I run the below test in which I intentionally use a name that is taken
@pytest.mark.django_db
def test_coop_type_create_with_existing(self):
""" Test coop type serizlizer model if there is already a coop type by that name """
coop_type = CoopTypeFactory()
serializer_data = {
"name": coop_type.name,
}
serializer = CoopTypeSerializer(data=serializer_data)
serializer.is_valid()
print(serializer.errors)
assert serializer.is_valid(), serializer.errors
result = serializer.save()
assert result.name == name
I get the below error
python manage.py test --settings=directory.test_settings
... ----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/davea/Documents/workspace/chicommons/maps/web/tests/test_serializers.py", line 46, in test_coop_type_create_with_existing
assert serializer.is_valid(), serializer.errors
AssertionError: {'name': [ErrorDetail(string='coop type with this name already exists.', code='unique')]}
How do I construct my serializer so that I can create my model if its unique key doesn't exist, or re-use it if it does?
Edit: Here's the GitHub link ...
https://github.com/chicommons/maps/tree/master/web
回答1:
DRF validates the uniqueness of each field if is declared with unique=True
in the model, so you have to change the model as following if you want to keep your unique contraint for the name
field:
class CoopType(models.Model):
name = models.CharField(max_length=200, null=False)
objects = CoopTypeManager()
class Meta:
# Creates a new unique constraint with the `name` field
constraints = [models.UniqueConstraint(fields=['name'], name='coop_type_unq')]
Also, you have to change your serializer, if you're using a ViewSet with the default behavior, you only need to add a custom validation in the serializer.
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from .models import CoopType
class CoopTypeSerializer(serializers.ModelSerializer):
default_error_messages = {'name_exists': 'The name already exists'}
class Meta:
model = CoopType
fields = ['id', 'name']
def validate(self, attrs):
validated_attrs = super().validate(attrs)
errors = {}
# check if the new `name` doesn't exist for other db record, this is only for updates
if (
self.instance # the instance to be updated
and 'name' in validated_attrs # if name is in the attributes
and self.instance.name != validated_attrs['name'] # if the name is updated
):
if (
CoopType.objects.filter(name=validated_attrs['name'])
.exclude(id=self.instance.id)
.exists()
):
errors['name'] = self.error_messages['name_exists']
if errors:
raise ValidationError(errors)
return validated_attrs
def create(self, validated_data):
# get_or_create returns a tuple with (instance, boolean). The boolean is True if a new instance was created and False otherwise
return CoopType.objects.get_or_create(**validated_data)[0]
The update
method was removed because is not needed.
Finally, the tests:
class FactoryTest(TestCase):
def test_coop_type_create_with_existing(self):
""" Test coop type serializer model if there is already a coop type by that name """
coop_type = CoopTypeFactory()
serializer_data = {
"name": coop_type.name,
}
# Creation
serializer = CoopTypeSerializer(data=serializer_data)
serializer.is_valid()
self.assertTrue(serializer.is_valid(), serializer.errors)
result = serializer.save()
assert result.name == serializer_data['name']
# update with no changes
serializer = CoopTypeSerializer(coop_type, data=serializer_data)
serializer.is_valid()
serializer.save()
self.assertTrue(serializer.is_valid(), serializer.errors)
# update with the name changed
serializer = CoopTypeSerializer(coop_type, data={'name': 'testname'})
serializer.is_valid()
serializer.save()
self.assertTrue(serializer.is_valid(), serializer.errors)
coop_type.refresh_from_db()
self.assertEqual(coop_type.name, 'testname')
回答2:
When you are using unique=True
key in model, Serializer will automaticly add unique validator to that field.
It’s enough to cancel the uniqueness check by writting your own name
field directly in serializer to prevent your curent error:
class Ser(serializers.ModelSerializer):
name = serializers.CharField() # no unique validation here
class Meta:
model = CoopType
fields = ['id', 'name']
def create(self, validated_data):
return CoopType.objects.get_or_create(**validated_data)
Be carefull: get_or_create
in create
method will return tuple, not instance.
Ok, now imagine you will call it with id
field too so you really need an update
method.
Then you can make the following hack in validate
method (maybe it's dirty, but it will work):
class Ser(serializers.ModelSerializer):
# no `read_only` option (default for primary keys in `ModelSerializer`)
id = serializers.IntegerField(required=False)
# no unique validators in charfield
name = serializers.CharField()
class Meta:
model = CoopType
fields = ["id", "name"]
def validate(self, attrs):
attrs = super().validate(attrs)
if "id" in attrs:
try:
self.instance = CoopType.objects.get(name=attrs["name"])
except CoopType.DoesNotExist:
pass
# to prevent manual changing ids in database
del attrs["id"]
return attrs
def create(self, validated_data):
return CoopType.objects.get_or_create(**validated_data)
def update(self, instance, validated_data):
# you can delete that method, it will be called anyway from parent class
return super().update(instance, validated_data)
The save
method on the serializer checks if the field self.instance
is null or not. If there is an non-empty self.instance
, it will call the update
method; else - create
method.
So if CoopType
with name from your serializer_data
dictionary exists - update
method will be called. In other case you will see create
method call.
回答3:
My suggestion is to not use a ModelSerializer
but instead use a vanilla serializer.
class CoopTypeSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(max_length=200, required=True, allow_blank=False)
def create(self, validated_data):
"""
Create and return a new `CoopType` instance, given the validated data.
"""
return CoopType.objects.get_or_create(**validated_data)[0]
def update(self, instance, validated_data):
"""
Update and return an existing `CoopType` instance, given the validated data.
"""
instance.name = validated_data.get('name', instance.name)
instance.save()
return instance
来源:https://stackoverflow.com/questions/62010284/how-do-i-create-a-serializer-that-reuses-a-unique-key-of-my-model