【问题标题】:Django Rest Framework POST Update if existing or createDjango Rest Framework POST 更新(如果存在或创建)
【发布时间】:2021-07-07 02:49:45
【问题描述】:

我是 DRF 的新手。 我阅读了 API 文档,也许很明显,但我找不到方便的方法。

我有一个Answer 对象,它与Question 具有一对一的关系。

在前端,我曾经使用 POST 方法创建一个发送到api/answers 的答案,并使用 PUT 方法来更新发送到例如api/answers/24

但我想在服务器端处理它。我只会向api/answers 发送一个POST 方法,DRF 将根据answer_idquestion_id(因为它是一对一的)检查对象是否存在。 如果是,它将更新现有答案,如果不是,它将创建一个新答案。

我不知道应该在哪里实现它。我应该在序列化程序或 ViewSet 或其他东西中覆盖 create() 吗?

这是我的模型、序列化程序和视图:

class Answer(models.Model):
    question = models.OneToOneField(
        Question, on_delete=models.CASCADE, related_name="answer"
    )
    answer = models.CharField(
        max_length=1, choices=ANSWER_CHOICES, null=True, blank=True
    )


class AnswerSerializer(serializers.ModelSerializer):
    question = serializers.PrimaryKeyRelatedField(
        many=False, queryset=Question.objects.all()
    )

    class Meta:
        model = Answer
        fields = ("id", "answer", "question")


class AnswerViewSet(ModelViewSet):
    queryset = Answer.objects.all()
    serializer_class = AnswerSerializer
    filter_fields = ("question", "answer")

【问题讨论】:

  • 如果已经有edit the object,则不会发布(如果 URL 中提到了 ID)?来自链接:“在某些情况下,使用 PUT 创建资源或使用 POST 更新资源是完全可能的、有效的甚至是首选的”。
  • 没有文章说,如果您在 url 中提供 id,则使用 PUT 否则使用 POST。所以我想使用POST。但是我希望它更新而不是尝试创建是否已经存在该实例。我希望它部分更新,所以也有。
  • 嗯——我一定是理解错了。我从文章的 cmets 中看到,我并不孤单 :) 也许这个 previous SO answer 可以帮助你呢?
  • 谢谢,但答案恰恰相反:“更新:只能通过以下方式使用 PUT 执行:...”

标签: python django django-rest-framework


【解决方案1】:

很遗憾,您提供和接受的答案没有回答您的原始问题,因为它不会更新模型。然而,这很容易通过另一种方便的方法实现:update-or-create

def create(self, validated_data):
    answer, created = Answer.objects.update_or_create(
        question=validated_data.get('question', None),
        defaults={'answer': validated_data.get('answer', None)})
    return answer

这应该在数据库中创建一个Answer 对象,如果具有question=validated_data['question'] 的对象不存在并且答案取自validated_data['answer']。如果已经存在,django 会将其 answer 属性设置为validated_data['answer']

正如 Nirri 的回答所指出的,这个函数应该驻留在序列化程序中。如果你使用通用的ListCreateView,它会在发送请求后调用create函数并生成相应的响应。

【讨论】:

  • 更新时返回HTTP_201_CREATED的唯一问题:(
【解决方案2】:

@Nirri 发布的答案对我也有帮助,但我使用 Django 的 QuerySet API 快捷方式找到了更优雅的解决方案:

def create(self, validated_data):
    answer, created = Answer.objects.get_or_create(
        question=validated_data.get('question', None),
        defaults={'answer': validated_data.get('answer', None)})

    return answer

它做同样的事情 - 如果 Answer 到那个 Question 不存在,它将被创建,否则它被 question 字段查找原样返回。

但是,此快捷方式不会更新对象。 QuerySet API 有另一种update 操作方法,称为update_or_create 并发布在other answer down the thread

【讨论】:

  • 正如 K Moe 在回答中指出的那样,这不会使用传递的新值更新对象。 stackoverflow.com/a/43393253/238166
  • @Inti:当然不是。它遵循更传统的 REST 方法,并作为更简洁的解决方案的参考,使用的快捷方式是 我已发布链接的 API 的一部分。当然,我会更新答案以避免误解。
【解决方案3】:

我会使用序列化程序的 create 方法。

您可以在其中检查问题(使用您在“问题”主键相关字段中提供的 ID)是否已经有答案,如果有,则获取对象并更新它,否则创建一个新的一个。

所以第一个选项是这样的:

class AnswerSerializer(serializers.ModelSerializer):
    question = serializers.PrimaryKeyRelatedField(many=False, queryset=Question.objects.all())

    class Meta:
        model = Answer
        fields = (
            'id',
            'answer',
            'question',
        )

    def create(self, validated_data):
        question_id = validated_data.get('question', None)
        if question_id is not None:
            question = Question.objects.filter(id=question_id).first()
            if question is not None:
                answer = question.answer
                if answer is not None:
                   # update your answer
                   return answer

        answer = Answer.objects.create(**validated_data)
        return answer

第二个选项是检查带有答案 id 的答案是否存在。

答案 ID 不会显示在发布请求的验证数据中,除非您使用某种变通方法并将它们手动定义为 read_only = false 字段:

id = serializers.IntegerField(read_only=False)

但是您应该重新考虑这一点,PUT 方法和 POST 方法作为单独的实体存在是有充分理由的,您应该将前端的请求分开。

【讨论】:

  • 非常感谢。第一个选项就足够了。我想将它们分开的原因是:如果有答案,我会在前面调用 UpdateAnswer 否则 CreateAnswer - 它在 React 中。所以让我们想象一下你和我在不同的浏览器中打开了这个网站。有一个未回答的问题,您单击“是”按钮并发送数据,以便服务器创建对象。我没有刷新浏览器并单击“是”或“否”,因此它将发送另一个失败的创建请求。所以我必须在服务器端处理它。这就是原因。
【解决方案4】:

一个更通用的答案,我认为这应该在视图集中而不是序列化器中,因为序列化器只需要序列化,仅此而已。

这模拟了update 将id 从request.data 传递给kwargs 的条件,因此如果实例不存在,UpdateModelMixin.update() 将引发Http404 异常,该异常被 except 块捕获并调用create().

from rest_framework.mixins import UpdateModelMixin
from django.http import Http404


class AnswerViewSet(UpdateModelMixin, ModelViewSet):
    queryset = Answer.objects.all()
    serializer_class = AnswerSerializer
    filter_fields = ("question", "answer")

    update_data_pk_field = 'id'

    def create(self, request, *args, **kwargs):
        kwarg_field: str = self.lookup_url_kwarg or self.lookup_field
        self.kwargs[kwarg_field] = request.data[self.update_data_pk_field]

        try:
            return self.update(request, *args, **kwargs)
        except Http404:
            return super().create(request, *args, **kwargs)

【讨论】:

  • 查看modelviewset,它已经包含了UpdateModelMixin。那么为什么要传递 ModelViewSet 呢?你不能只传递 UpdateModelMixin 和 CreateModelMixin 吗?
  • 这确实应该是公认的答案,这比需要在update_or_create() 方法中指定每个参数要好得多。感谢您的解决方案!
【解决方案5】:

还有:

try:
   serializer.instance = YourModel.objects.get(...)
except YourModel.DoesNotExist:
   pass

if serializer.is_valid():
   serializer.save()    # will INSERT or UPDATE your validated data

【讨论】:

    【解决方案6】:

    我尝试了序列化程序解决方案,但在点击序列化程序函数create(self, validated_data) 之前似乎引发了异常。那是因为我正在使用ModelViewSet(它又使用class CreatedModelMixin)。进一步的研究表明这里提出了异常:

    rest_framework/mixins.py
    
    class CreateModelMixin(object):
        def create(self, request, *args, **kwargs):
            serializer = self.get_serializer(data=request.data)
            serializer.is_valid(raise_exception=True) <== Here
    

    由于我想保留框架提供的所有功能,所以我更喜欢捕获异常并路由更新:

    from rest_framework.exceptions import ValidationError
    
    class MyViewSet(viewsets.ModelViewSet)
    
        def create(self, request, *args, **kwargs):
            pk_field = 'uuid'
            try:
                response = super().create(request, args, kwargs)
            except ValidationError as e:
                codes = e.get_codes()
                # Check if error due to item exists
                if pk_field in codes and codes[pk_field][0] == 'unique':
                    # Feed the lookup field otherwise update() will failed
                    lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
                    self.kwargs[lookup_url_kwarg] = request.data[pk_field]
                    return super().update(request, *args, **kwargs)
                else:
                    raise e
            return response
    

    我的应用始终可以使用参数调用POST /api/my_model/(这里,uuid = 主键)。

    但是,如果我们在update 函数中处理这个会更好吗?

        def update(self, request, *args, **kwargs):
            try:
                response = super().update(request, *args, **kwargs)
            except Http404:
                mutable = request.data._mutable
                request.data._mutable = True
                request.data["uuid"] = kwargs["pk"]
                request.data._mutable = mutable
                return super().create(request, *args, **kwargs)
            return response
    

    【讨论】:

      【解决方案7】:

      一个更好和更通用的应用方法是使用潜在实例更新 ModelSerializer 对象(如果存在)。这允许 DRF 遵循标准协议,并且可以轻松地跨模型抽象。

      为了保持通用性,首先让 UpdateOrCreate 类在实例化时与 modelSerializer 一起被继承。在此,添加 def update_or_create_helper

      然后为您希望使用该功能的每个 Serializer 继承 UpdateOrCreate 类,并添加一个特定于该模型的简单 is_valid def。

      序列化器.py

      class UpdateOrCreate:
          def update_or_create_helper(self, obj_model, pk):
              # Check to see if data has been given to the serializer
              if hasattr(self, 'initial_data'):
                  # Pull the object from the db
                  obj = obj_model.objects.filter(pk=self.initial_data[pk])
                  # Check if one and only one object exists with matching criteria
                  if len(obj)==1:
                      # If you want to allow for partial updates
                      self.partial = True
                      # Add the current instance to the object
                      self.instance = obj[0]
              # Continue normally
              return super().is_valid()
      
      ...
      
      # Instantiate the model with your standard ModelSerializer 
      # Inherit the UpdateOrCreate class
      class MyModelSerializer(serializers.ModelSerializer, UpdateOrCreate):
          class Meta:
              model = MyModel
              fields = ['pk', 'other_fields']
          # Extend is_valid to include the newly created update_or_create_helper
          def is_valid(self):
              return self.update_or_create_helper(obj_model=MyModel, pk='pk')
      

      【讨论】:

        【解决方案8】:

        这个 mixin 将允许在 ListSerializer 中使用 create 或 update

        class CreateOrUpdateMixin(object):
            def __init__(self, *args, **kwargs):
                super().__init__(*args, **kwargs)
                # check if self.pk key is in Meta.fields, if not append it
                if self.Meta.model._meta.pk.name not in self.Meta.fields:
                    self.Meta.fields.append(self.Meta.model._meta.pk.name)
                # init pk field on serializer (field will be named accordingly to your pk name)
                # specify serializers.IntegerField if you use models.AutoField
                self._declared_fields[self.Meta.model._meta.pk.name] = serializers.UUIDField(required=False)
        
            def create(self, validated_data):
                obj, created = self.Meta.model.objects.update_or_create(
                    pk=validated_data.pop(self.Meta.model._meta.pk.name, None),
                    defaults={**validated_data}
                )
                return obj
        

        使用方法:

        class DatacenterListSerializer(CreateOrUpdateMixin, serializers.ModelSerializer):
            class Meta:
                model = Datacenter
                fields = ['somefield', 'somefield2']
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-05-03
          • 2020-11-11
          • 2012-12-16
          • 1970-01-01
          • 1970-01-01
          • 2018-01-08
          • 1970-01-01
          • 2019-05-29
          相关资源
          最近更新 更多