Django Rest Framework POST Обновление, если существующее или созданное

Я новичок в DRF. Я читал документы API, возможно, это не замечательно, но я не мог найти удобный способ сделать это.

У меня есть объект Answer, который имеет отношение "один к одному" с вопросом.

На лицевой стороне я использовал метод POST для создания ответа, отправленного на api/answers, и метод PUT для обновления, отправленного, например. апи/ответы/24

Но я хочу обработать его на стороне сервера. Я только отправлю метод POST в api/answers, и DRF проверит на основе answer_id или question_id (поскольку это один к одному), если объект существует. Если это произойдет, оно обновит существующее, если оно не создаст новый ответ.

Где я должен это реализовать, я не мог понять. Переопределить создание в сериализаторе или в ViewSet или что-то еще?

Моя модель, сериализатор и представление выглядят следующим образом:

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

К сожалению, ваш предоставленный и принятый ответ не отвечает на ваш первоначальный вопрос, так как он не обновляет модель. Это, однако, легко достигается с помощью другого метода удобства: 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'].

Как отмечено в ответе Нирри, эта функция должна находиться внутри сериализатора. Если вы используете общий ListCreateView, он вызовет функцию create после отправки почтового запроса и создания соответствующего ответа.

Ответ 2

Мне также помог ответ @Nirri, но я нашел более элегантное решение с помощью 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 и опубликован в другом ответе вниз по цепочке.

Ответ 3

Я бы использовал метод создания сериализаторов.

В нем вы можете проверить, есть ли у ответа на вопрос (с идентификатором, который вы задаете в поле, связанном с первичным ключом "вопрос"), и, если он есть, получить объект и обновить его, в противном случае создать новый.

Таким образом, первый вариант будет выглядеть примерно так:

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

Второй вариант - проверить, существует ли ответ с идентификатором ответа.

Идентификатор ответа не будет отображаться в проверенных данных запросов на публикацию, если вы не использовали своего рода обходной путь и не определили их вручную как поля read_only = false:

id = serializers.IntegerField(read_only=False)

Но вы должны все же переосмыслить это. Есть веская причина, по которой метод PUT и методы POST существуют как отдельные объекты, и вы должны разделять запросы на внешнем интерфейсе.

Ответ 4

Также:

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

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

Ответ 5

Я попробовал решение для сериализатора, но, похоже, возникла исключительная ситуация, прежде чем он включил функцию сериализатора 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

Ответ 6

Лучший и более обобщенный способ применить это - обновить объект 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')