Django Rest Framework的POST更新:如果存在则更新,不存在则创建

58

我是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")

1
如果已经存在一个对象(假设在URL中提到了id),则不会将其发布为POST。从链接中可以看出:“在某些情况下,使用PUT创建资源或使用POST更新资源是完全可能、有效甚至更好的方法”。 - Mathieu Dhondt
1
不,文章说如果在URL中提供了ID,则使用PUT,否则使用POST。所以我想使用POST。但是我希望它更新而不是尝试创建已经存在的实例。并且我希望它部分更新,这也是需要考虑的。 - Ali Ankarali
嗯,我可能错解了。看到文章的评论区,发现并不只有我一个人 :) 也许这个之前的SO回答可以帮助你吧? - Mathieu Dhondt
谢谢,但是那个答案说的相反:"更新:只能通过以下方式使用PUT执行..." - Ali Ankarali
9个回答

64
很遗憾,您提供和接受的答案并没有回答您的原始问题,因为它没有更新模型。然而,通过另一个便利方法可以轻松实现这一点: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

如果数据库中不存在一个问题为validated_data ['question']的Answer对象,则应该创建一个Answer对象,并从validated_data ['answer']中获取答案。如果它已经存在,django将设置其答案属性为validated_data ['answer']。正如Nirri的回答所指出的那样,这个函数应该驻留在序列化器内。如果您使用通用的ListCreateView,它将在发送POST请求时调用create函数并生成相应的响应。

3
更新时唯一的问题是它返回了 HTTP_201_CREATED :( - Artem Bernatskyi
2
我认为这个方向不对。Update_or_create 应该在视图层而不是序列化器层完成。 - gabn88

18

我也从@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

它执行的是完全相同的操作 - 如果QuestionAnswer不存在,它将被创建,否则它将按原样通过question字段查找返回。

然而,这个快捷方式不会更新对象。 QuerySet API还有另一种进行update操作的方法,称为update_or_create,并发布在线程下的其他答案中


1
正如K Moe的回答中所指出的那样,这不会使用传递的新值更新对象。https://dev59.com/LVoU5IYBdhLWcg3wG0Ky#43393253 - Inti
1
@Inti:当然不是。它遵循更传统的REST方法,并作为使用我发布链接中的API的快捷方式的更清洁解决方案的参考。当然,我会更新答案以避免误解。 - Damaged Organic

9
我会使用序列化程序的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的答案是否存在。
除非您使用某种解决方法并手动将其定义为read_only = false字段,否则答案ID不会显示在POST请求的验证数据中:
id = serializers.IntegerField(read_only=False)

但是你应该重新考虑一下,PUT方法和POST方法被设计成独立实体是有很好的理由的,你应该在前端将这些请求分开处理。


3
非常感谢。第一个选项就足够了。我之所以想要将它们分开,是因为:如果有答案,我调用UpdateAnswer,否则调用CreateAnswer——这是在React中进行的。假设你和我在不同的浏览器中打开网站,有一个未答复的问题,你点击了“是”按钮并发送了数据,服务器将创建对象。我没有刷新浏览器,然后点击了“是”或“否”,这将发送另一个创建请求,但会失败。所以我必须在服务器端处理它。这就是原因。 - Ali Ankarali

8

我认为应该将这个功能放在视图集中而不是序列化器中,因为序列化器只需要进行序列化处理,而不需要更多的功能。

这模拟了使用从request.data传递id到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)

1
看着ModelViewSet,它已经包含了UpdateModelMixin。那么为什么要传递ModelViewSet呢?难道你不能只传递UpdateModelMixin和CreateModelMixin吗? - Jose Georges
1
这真的应该成为被接受的答案,这比需要在update_or_create()方法中指定每个参数的方法要好得多。感谢您提供的解决方案! - Josh Correia

2
另外:

也:

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

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

2
更好、更通用的方法是,如果存在潜在实例,则更新 ModelSerializer 对象。这样 DRF 就可以遵循标准协议,并且可以轻松地在多个模型之间抽象。
为了保持通用性,在实例化时,首先创建一个 UpdateOrCreate 类,该类将与 modelSerializer 一起继承。在其中添加 def update_or_create_helper。
然后为每个需要此功能的 Serializer 继承 UpdateOrCreate 类,并添加一个特定于该模型的简单 is_valid def。
serializers.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')

1
由于多重继承,您需要更改超级调用为:return super(serializers.ModelSerializer, self).is_valid()。这样对我有效。 - ramigg

1

我尝试过使用序列化器的解决方案,但似乎在调用序列化函数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

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

但是,如果我们在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

1
这个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']

0
如果您在models.ForeignKey中使用to_field(例如:task_id),则需要添加lookup_field = 'task_id',如下所示。
# views.py
class XXXViewSet(viewsets.ModelViewSet):
    queryset = XXX.objects.all()
    serializer_class = XXXSerializer

    update_data_pk_field = 'task_id'
    lookup_field = 'task_id'

    # update or create
    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)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接