python Django Rest框架POST更新(如果存在)或创建

icnyk63a  于 2023-02-11  发布在  Python
关注(0)|答案(9)|浏览(122)

我是DRF的新手。我读了API文档,也许这是显而易见的,但我找不到一个方便的方法来做。
我有一个Answer对象,它与Question有一对一的关系。
在前端,我使用POST方法创建发送到api/answers的答案,使用PUT方法更新发送到api/answers/24的答案
但是我想在服务器端处理它,我只发送一个POST方法到api/answers,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")
avkwfej4

avkwfej41#

很遗憾,您提供并接受的答案并没有回答您的原始问题,因为它不会更新模型。不过,这可以通过另一种方便的方法轻松实现:更新或创建

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

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

vlurs2pr

vlurs2pr2#

@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,并在线程的其他应答中发布。

l2osamch

l2osamch3#

我将使用序列化程序的create方法。
在其中,您可以检查问题(使用您在“question”主键相关字段中提供的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方法作为单独的实体存在是有充分理由的,您应该在前端分离请求。

x6yk4ghg

x6yk4ghg4#

更一般的答案是,我认为这应该在视图集中而不是在序列化程序中,因为序列化程序只需要序列化,仅此而已。
这模拟了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)
4xy9mtcn

4xy9mtcn5#

一个更好、更通用的方法是用一个潜在的示例更新ModelSerializer对象(如果它存在的话),这允许DRF遵循标准协议,并且可以很容易地跨模型进行抽象。
为了保持通用性,首先创建一个UpdateOrCreate类,在示例化时与modelSerializer一起继承,其中添加def update_or_create_helper
然后,为每个您希望其具有功能的串行器继承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')
fkaflof6

fkaflof66#

还有:

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

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

wdebmtf27#

我尝试了序列化器解决方案,但似乎在命中序列化器函数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
4smxwvx5

4smxwvx58#

此混合将允许在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']
nr7wwzry

nr7wwzry9#

如果在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)

相关问题