【问题标题】:Django rest framework get or create for PrimaryKeyRelatedFieldDjango rest 框架获取或创建 PrimaryKeyRelatedField
【发布时间】:2017-10-13 07:47:24
【问题描述】:

我开始使用 Django 和 Django REST 框架为我的 Web 应用程序创建 REST API,我需要一个逻辑问题。

有实体指令和标签。用户访问我的服务并创建自我指令并为其添加现有标签或新标签。

我使用 PrimaryKeyRelatedField 为关系指令创建了我的模型序列化程序类标签。但是,如果我为带有新标签的新指令进行 POST,则会收到错误:“Invalid pk \"tagname\" - 对象不存在。”。 我通过覆盖我的字段类中的 to_internal_value 方法解决了这个问题。

解决此问题的最佳做法是什么?在我看来,这个问题对于 Web 和 REST API 来说很典型。

我的模型:

class Tag(Model):
    name = CharField(max_length=32, verbose_name=_("Name"),
                     unique=True, validators=[alphanumeric], primary_key=True)

    def __str__(self):
        return self.name


class Instruction(Model):
    user = ForeignKey(settings.AUTH_USER_MODEL,
                      related_name='instructions',
                      on_delete=CASCADE,
                      blank=False, null=False,
                      verbose_name=_("User"))
    title = CharField(max_length=256,
                      verbose_name=_("Title"),
                      blank=False, null=False)
    created_datetime = DateTimeField(verbose_name=_("Creation time"), editable=False)
    modified_datetime = DateTimeField(
        verbose_name=_("Last modification time"), blank=False, null=False)
    tags = ManyToManyField(Tag,
                           related_name="instructions",
                           verbose_name=_("Tags"))

    class Meta:
        ordering = ['-created_datetime']
        # singular_name = _("")

    def save(self, force_insert=False, force_update=False, using=None,
             update_fields=None):
        n = now()
        if self.id is None:
            self.created_datetime = n
        self.modified_datetime = n
        super(Instruction, self).save(force_insert, force_update, using, update_fields)

    def __str__(self):
        return self.title

我的序列化器:

class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ('name',)

class InstructionSerializer(serializers.ModelSerializer):
    tags = PrimaryKeyCreateRelatedField(many=True, queryset=Tag.objects.all())
    author = serializers.SerializerMethodField()

    def get_author(self, obj):
        return obj.user.username

    class Meta:
        model = Instruction
        fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'author')
        read_only_fields = ('modified_datetime',)

我创建了新的字段类类 PrimaryKeyCreateRelatedField 并重写了 to_internal_value 方法来创建新的 Tag 对象,而不是使用消息“does_not_exist”引发:

PrimaryKeyCreateRelatedField(serializers.PrimaryKeyRelatedField):

    def to_internal_value(self, data):
        if self.pk_field is not None:
            data = self.pk_field.to_internal_value(data)
        try:
            return self.get_queryset().get(pk=data)
        except ObjectDoesNotExist:
            # self.fail('does_not_exist', pk_value=data)
            return self.get_queryset().create(pk=data)
        except (TypeError, ValueError):
            self.fail('incorrect_type', data_type=type(data).__name__)

我的看法:

class InstructionViewSet(viewsets.ModelViewSet):
    queryset = Instruction.objects.all()
    serializer_class = InstructionSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

    def create(self, request, *args, **kwargs):
        data = dict.copy(request.data)
        data['user'] = self.request.user.pk

        serializer = InstructionSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

更新

models.py

alphanumeric = RegexValidator(r'^[0-9a-zA-Z]*$',
                              _('Only alphanumeric characters are allowed.'))


class Tag(Model):
    name = CharField(max_length=32, verbose_name=_("Name"),
                     unique=True, validators=[alphanumeric], primary_key=True)

    def __str__(self):
        return self.name


class Step(PolymorphicModel):
    instruction = ForeignKey(Instruction,
                             verbose_name=_("Instruction"),
                             related_name='steps',
                             blank=False, null=False,
                             on_delete=CASCADE)
    position = PositiveSmallIntegerField(verbose_name=_("Position"), default=0)

    description = TextField(verbose_name=_("Description"),
                            max_length=2048,
                            blank=False, null=False)

    class Meta:
        verbose_name = _("Step")
        verbose_name_plural = _("Steps")
        ordering = ('position',)
        unique_together = ("instruction", "position")

    def __str__(self):
        return self.description[:100]


class Instruction(Model):
    user = ForeignKey(settings.AUTH_USER_MODEL,
                      related_name='instructions',
                      on_delete=CASCADE,
                      blank=False, null=False,
                      verbose_name=_("User"))
    title = CharField(max_length=256,
                      verbose_name=_("Title"),
                      blank=False, null=False)
    created_datetime = DateTimeField(verbose_name=_("Creation time"), editable=False)
    modified_datetime = DateTimeField(
        verbose_name=_("Last modification time"), blank=False, null=False)
    tags = ManyToManyField(Tag,
                           related_name="instructions",
                           verbose_name=_("Tags"))

    # thumbnail = #TODO: image field

    class Meta:
        ordering = ['-created_datetime']
        # singular_name = _("")

    def save(self, force_insert=False, force_update=False, using=None,
             update_fields=None):
        n = now()
        if self.id is None:
            self.created_datetime = n
        self.modified_datetime = n
        super(Instruction, self).save(force_insert, force_update, using, update_fields)

    def __str__(self):
        return self.title

views.py

class InstructionViewSet(viewsets.ModelViewSet):
    queryset = Instruction.objects.all()
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

    def get_serializer_class(self):
        """Return different serializer class for different action."""
        if self.action == 'list':
            return InstructionSerializer
        elif self.action == 'create':
            return InstructionCreateSerializer

serialiers.py

class PrimaryKeyCreateRelatedField(serializers.PrimaryKeyRelatedField):

    def to_internal_value(self, data):
        if self.pk_field is not None:
            data = self.pk_field.to_internal_value(data)
        try:
            return self.get_queryset().get(pk=data)
        except ObjectDoesNotExist:
            # self.fail('does_not_exist', pk_value=data)
            return self.get_queryset().create(pk=data)
        except (TypeError, ValueError):
            self.fail('incorrect_type', data_type=type(data).__name__)


class InstructionCreateSerializer(serializers.ModelSerializer):
    tags = PrimaryKeyCreateRelatedField(many=True, queryset=Tag.objects.all())
    steps = InstructionStepSerializer(many=True)
    user = serializers.HiddenField(default=serializers.CurrentUserDefault())

    class Meta:
        model = Instruction
        fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'steps')
        read_only_fields = ('modified_datetime',)

    def create(self, validated_data):
        tags_data = validated_data.pop('tags')
        steps_data = validated_data.pop('steps')

        # NOTE: tags need add after creation of the Instruction object otherwise we will got exception:
    # "needs to have a value for field "id" before this many-to-many relationship can be used."
        instruction = Instruction.objects.create(**validated_data)

        for tag in tags_data:
            instruction.tags.add(tag)

        for step in steps_data:
            Step.objects.create(instruction=instruction,
                                description=step['description'],
                                position=step['position'])
        return instruction


class InstructionSerializer(serializers.ModelSerializer):
    tags = serializers.StringRelatedField(many=True)
    author = serializers.SerializerMethodField()
    steps = InstructionStepSerializer(many=True)

    def get_author(self, obj):
        return obj.user.username

    class Meta:
        model = Instruction
        fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'author', 'steps')
        read_only_fields = ('modified_datetime',)

【问题讨论】:

    标签: django rest tags django-rest-framework relationship


    【解决方案1】:

    除了@YPCrumble 和@SijanBhandari 给出的答案,我只需要对您代码中的某些内容进行评论。

    在models.py 中,您已经覆盖了添加created_at 和modified_on 的save 方法。为此,您可以添加

    created_at = models.DateTimeField(auto_now_add=True)
    modified_on = DateTimeField (auto_now=True)
    

    auto_now_add 选项在第一次创建对象时设置。 它不可编辑。 auto_now 设置在对象被保存时设置,即在 object.save() 方法被调用时设置。

    这些通常用于为对象添加时间戳以供将来参考。

    为什么要写这么多行,而您只需要 2 行代码就可以做到这一点。 只是提醒一下!

    欲知详情,go to the documentation here

    【讨论】:

    • 感谢您的评论!我将来会考虑到这一点。
    【解决方案2】:

    就我而言,要解决问题,我需要重写方法run_validation。这允许检查tags 并在验证之前创建它们(如果不存在)。

    class InstructionCreateSerializer(serializers.ModelSerializer):
        steps = InstructionStepSerializer(many=True)
        user = serializers.HiddenField(default=serializers.CurrentUserDefault())
    
        class Meta:
            model = Instruction
            fields = ('title', 'created_datetime', 'modified_datetime', 'tags', 'steps', 'id', 'user')
            read_only_fields = ('modified_datetime',)
    
        def run_validation(self, data=serializers.empty):
            if 'tags' in data:
                for tag in data['tags']:
                    Tag.objects.get_or_create(name=tag)
            return super(InstructionCreateSerializer, self).run_validation(data)
    
        def create(self, validated_data):
            tags_data = validated_data.pop('tags')
            steps_data = validated_data.pop('steps')
    
            # NOTE: tags need add after creation of the Instruction object otherwise we will got exception:
            # "needs to have a value for field "id" before this many-to-many relationship can be used."
            instruction = Instruction.objects.create(**validated_data)
    
            for tag in tags_data:
                instruction.tags.add(tag)
    
            for step in steps_data:
                Step.objects.create(instruction=instruction,
                                    description=step['description'],
                                    position=step['position'])
            return instruction
    

    【讨论】:

      【解决方案3】:

      在“常规”Django 中,您通常希望在表单的save 方法中创建模型实例,而不是视图。 DRF 与此类似,因为您希望在序列化程序的 createupdate 方法中创建模型实例。这样做的原因是,如果您需要向 API 添加新端点,您可以重用序列化程序,而不必编写重复的代码来创建或更新模型实例。

      以下是我重构代码的方式:

      • 从您的 ModelViewSet 中删除整个 create 方法 - 您不需要覆盖它。
      • 删除自定义PrimaryKeyCreateRelatedField - 你只需要一个PrimaryKeyRelatedField
      • 向您的序列化程序添加两个方法 - createupdate
        • create 方法中,在保存instruction 对象之前创建tag 对象,如您所见in the DRF docs。您可以通过 self.context['request'].user 在此 create 方法中获取当前用户,就像您在视图中所做的那样。所以你可以像Instruction.objects.create(user=self.context['request'].user, **validated_data)一样创建Instruction,然后循环遍历tags(就像他们在文档中为tracks所做的那样)将它们添加到Instruction
        • 文档没有示例update 方法,但本质上您的update 方法也为现有instruction 采用instance 参数。详情请见this answer from the creator of DRF

      【讨论】:

      • 感谢您的回答。我正在尝试您的解决方案(将标签的创建从我的 ModelViewSet 移动到我的 InstructionSerializer 中)但我收到错误:“用户”:[“此字段是必需的。”]。我认为这是因为验证是在调用序列化程序中的创建/更新方法之前进行的。何时验证字段“用户”为空。如何解决?
      • @StanZeez 如果您通过登录用户添加user,则应从InstructionSerializer.Meta.fields 声明中删除“用户”。
      • 我理解你,但是如果我从我的指令序列化程序中删除“用户”,我如何在 GET 响应中显示指令的作者?是否可以通过覆盖 InstructionViewSet 中的 get_serializer_class 方法来为 createlist 操作使用不同的序列化程序?
      • 现在您正在为此使用get_author 方法。一种选择是使用CurrentUserDefault 将当前用户用作该字段的作者。
      • 请查看我的更新(我添加了 Step 模型以完成图片)。这是我在所有变化之后得到的。我仍然有 PrimaryKeyRelease Relied 的问题。如果我将自定义 PrimaryCareReleaseField 更改为 serializers.PrimaryKeRelatedField,我会收到错误消息:"tags": [ "Invalid pk \"tagname\" - object does not exist."]。一种解决方案是使用 TagSerializer 字段而不是基于 PrimaryKeRelatedField 的字段。但随后我必须将 POST 标记对象作为 {"name": "tagname"} 传递,而不是简单地字符串 "tagname"。
      【解决方案4】:

      最好的方法是在视图的 CREATE 方法中整理所有内容。

      我相信你的标签会以

      的格式从你的前端发送到后端
      [   1,
          {'name': "TEST"},
          {'name': 'TEST2'}
      ]
      

      这里'1' 是现有的标签ID,'TEST''TEST2' 是插入的两个新标签 用户。现在您可以按如下方式更改您的 CREATE 方法:

      class InstructionViewSet(viewsets.ModelViewSet):
          queryset = Instruction.objects.all()
          serializer_class = InstructionSerializer
          permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
      
          def create(self, request, *args, **kwargs):
              data = dict.copy(request.data)
              data['user'] = self.request.user.pk
      
              # MODIFICATION.....
              tags = self.request.get('tags', None)
              tag_list = []
              if tags:
                  for tag in tags:
                      if isinstance(tag, dict):
                          new_tag = Tag.objects.create(name=tag['name'])
                          tag_list.append(new_tag.id)
                      else:
                          tag_list.append(int(tag))
      
      
              data = {
      
              'title': ....
              'tags': tag_list,
              'user': ...
              'author': ...
              ......
      
      
      
              }
      
              serializer = InstructionSerializer(data=data)
      

      希望对你有帮助。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-11-19
        • 1970-01-01
        • 1970-01-01
        • 2020-05-15
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多