Django Rest Framework POST更新如果存在或创建

时间:2016-06-15 10:48:20

标签: python django django-rest-framework

我是DRF的新手。 我阅读了API文档,也许它没有注意到,但我找不到方便的方法。

我有一个Answer对象与问题有一对一的关系。

在正面我曾经使用POST方法创建发送到api / answers的答案,并且PUT方法更新发送到例如API /答案/ 24

但我想在服务器端处理它。我只会向api / answers发送POST方法,如果对象存在,DRF将根据answer_id或question_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',)

8 个答案:

答案 0 :(得分:24)

不幸的是,您提供和接受的答案无法回答您的原始问题,因为它不会更新模型。然而,这可以通过另一种方便的方法轻松实现: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会将其答案属性设置为validated_data['answer']

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

答案 1 :(得分:11)

@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

它完全相同 - 如果AnswerQuestion不存在,则会创建,否则 - 按question字段查找返回。

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

答案 2 :(得分: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方法作为单独的实体存在是一个很好的理由,你应该在前端分开请求。

答案 3 :(得分:3)

一个更通用的答案,我认为这应该在视图集中而不是序列化程序中,因为序列化程序只需要序列化就可以了。

这将模拟条件,update将id从request.data传递到kwargs,如果实例不存在,则UpdateModelMixin.update()会引发Http404例外除外块捕获的内容并执行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)

答案 4 :(得分: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

答案 5 :(得分:1)

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

要保持通用性,请首先在实例化时使UpdateOrCreate类与modelSerializer一起继承。在其中添加def update_or_create_helper

然后为每个要使用其功能的序列化器继承UpdateOrCreate类,并为该模型添加一个简单的is_valid定义。

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')

答案 6 :(得分:0)

也:

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

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

答案 7 :(得分:0)

此混入将允许在ListSerializer中使用创建或更新

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']