如何创建一个序列化程序来重用我的模型的唯一键?

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