如何创建一个序列化程序来重用我的模型的唯一键?
How do I create a serializer that reuses a unique key of my model?
我正在使用 Python 3.7、Django 2.2、Django rest 框架和 pytest。我有以下模型,如果现有模型通过其唯一键存在,我想在其中重新使用它 ...
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()
然后我创建了下面的序列化器来从 REST 数据生成这个模型
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
但是,当我 运行 下面的测试中我故意使用了一个名字
@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
我收到以下错误
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')]}
如何构建我的序列化程序,以便在其唯一键不存在时创建我的模型,或者在存在时重新使用它?
编辑: 这是 GitHub link ...
https://github.com/chicommons/maps/tree/master/web
当您在模型中使用 unique=True
键时,Serializer 会自动向该字段添加唯一验证器。
通过直接在序列化程序中编写您自己的 name
字段来取消唯一性检查就足以防止您当前的错误:
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)
注意:create
方法中的 get_or_create
将 return 元组,而不是实例。
好的,现在假设您也将使用 id
字段调用它,因此您确实需要一个 update
方法。
然后你可以在 validate
方法中进行以下破解(也许它很脏,但它会起作用):
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)
序列化器上的 save
方法检查字段 self.instance
是否为空。如果有非空的self.instance
,就会调用update
方法;否则 - create
方法。
因此,如果 CoopType
的名称来自您的 serializer_data
字典存在 - update
方法将被调用。在其他情况下,您将看到 create
方法调用。
如果在模型中用 unique=True
声明,DRF 会验证每个字段的唯一性,因此如果要保持 name
字段的唯一性约束,则必须按以下方式更改模型:
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')]
此外,您必须更改序列化程序,如果您使用的是具有默认行为的 ViewSet,则只需在序列化程序中添加自定义验证。
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]
update
方法已删除,因为不需要。
最后,测试:
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')
我的建议是不要使用 ModelSerializer
,而是使用 vanilla 序列化程序。
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
我正在使用 Python 3.7、Django 2.2、Django rest 框架和 pytest。我有以下模型,如果现有模型通过其唯一键存在,我想在其中重新使用它 ...
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()
然后我创建了下面的序列化器来从 REST 数据生成这个模型
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
但是,当我 运行 下面的测试中我故意使用了一个名字
@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
我收到以下错误
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')]}
如何构建我的序列化程序,以便在其唯一键不存在时创建我的模型,或者在存在时重新使用它?
编辑: 这是 GitHub link ...
https://github.com/chicommons/maps/tree/master/web
当您在模型中使用 unique=True
键时,Serializer 会自动向该字段添加唯一验证器。
通过直接在序列化程序中编写您自己的 name
字段来取消唯一性检查就足以防止您当前的错误:
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)
注意:create
方法中的 get_or_create
将 return 元组,而不是实例。
好的,现在假设您也将使用 id
字段调用它,因此您确实需要一个 update
方法。
然后你可以在 validate
方法中进行以下破解(也许它很脏,但它会起作用):
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)
序列化器上的 save
方法检查字段 self.instance
是否为空。如果有非空的self.instance
,就会调用update
方法;否则 - create
方法。
因此,如果 CoopType
的名称来自您的 serializer_data
字典存在 - update
方法将被调用。在其他情况下,您将看到 create
方法调用。
如果在模型中用 unique=True
声明,DRF 会验证每个字段的唯一性,因此如果要保持 name
字段的唯一性约束,则必须按以下方式更改模型:
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')]
此外,您必须更改序列化程序,如果您使用的是具有默认行为的 ViewSet,则只需在序列化程序中添加自定义验证。
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]
update
方法已删除,因为不需要。
最后,测试:
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')
我的建议是不要使用 ModelSerializer
,而是使用 vanilla 序列化程序。
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