web-dev-qa-db-fra.com

Cadre de repos Django POST Mettre à jour si existant ou créer

Je suis nouveau sur DRF ... Je lis la documentation de l’API, c’est peut-être inconscient, mais je n’ai pas trouvé un moyen pratique de le faire.

J'ai un objet Answer qui entretient une relation personnelle avec une question.

Sur le recto, j’utilisais la méthode POST pour créer une réponse envoyée à api/answers et la méthode PUT pour la mise à jour envoyée à, par exemple. api/réponses/24

Mais je veux le gérer du côté du serveur. Je n'enverrai qu'une méthode POST à api/answers et DRF vérifiera en fonction de answer_id ou de question_id (puisqu'il s'agit d'une réponse à une) si l'objet existe. Si c'est le cas, il mettra à jour l'existant, sinon il créera une nouvelle réponse.

Où je devrais le mettre en application, je ne pouvais pas comprendre. Remplacer créer dans le sérialiseur ou dans ViewSet ou quelque chose d'autre?

Mon modèle, mon sérialiseur et ma vue sont les suivants:

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',)
11
Ali Ankarali

La réponse publiée par @Nirri m'a aussi aidé, mais j'ai trouvé une solution plus élégante avec Django QuerySet API raccourci:

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

Il fait exactement la même chose - si Answer à cette Question n'existe pas, il sera créé, sinon - renvoyé tel quel par la recherche dans le champ question.

Ce raccourci, cependant, ne mettra pas à jour l'objet. QuerySet API a une autre méthode pour une opération update, appelée update_or_create et publiée dans autre réponse en aval du fil .

9
Damaged Organic

Malheureusement, votre réponse fournie et acceptée ne répond pas à votre question initiale, car elle ne met pas à jour le modèle. Ceci est cependant facilement réalisable par une autre méthode pratique: 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

Cela devrait créer un objet Answer dans la base de données s'il en existe un avec question=validated_data['question'] avec la réponse extraite de validated_data['answer']. S'il existe déjà, Django définira son attribut answer sur validated_data['answer'].

Comme indiqué dans la réponse de Nirri, cette fonction devrait résider dans le sérialiseur. Si vous utilisez le nom générique ListCreateView , la fonction de création sera appelée une fois la demande d'envoi envoyée et générera la réponse correspondante.

20
K Moe

J'utiliserais la méthode create des sérialiseurs. 

Vous pouvez y vérifier si la question (avec l'ID de celle-ci que vous indiquez dans le champ relatif à la clé primaire de la question) a déjà une réponse. Si c'est le cas, récupérez l'objet et mettez-le à jour, sinon créez-en un nouveau.

Donc, la première option serait quelque chose comme:

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

La deuxième option serait de vérifier si la réponse avec l'identifiant de réponse existe.

Les ID de réponse n'apparaissent pas dans les données validées des demandes de publication, à moins que vous n'ayez utilisé une sorte de solution de contournement et que vous les ayez définis manuellement comme des champs read_only = false:

id = serializers.IntegerField(read_only=False)

Mais vous devriez cependant repenser cela de fond en bout. Il y a une bonne raison pour que la méthode PUT et les méthodes POST existent en tant qu'entités séparées, et vous devez séparer les demandes sur le front-end.

3
xtrinch

J'ai essayé la solution de sérialiseur, mais il semble qu'une exception soit levée avant d'appuyer sur la fonction de sérialiseur create(self, validated_data). C'est parce que j'utilise ModelViewSet (qui utilise à son tour class CreatedModelMixin). Une étude plus approfondie révèle que l'exception évoquée ici:

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

Puisque je veux conserver toutes les fonctionnalités fournies par le framework, je préfère donc capturer les exceptions et acheminer par la route pour mettre à jour:

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

Mon application peut toujours appeler POST /api/my_model/ avec des paramètres (ici, uuid = clé primaire).

Cependant, serait-il préférable de gérer cela dans la fonction 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
John Pang

Également:

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

if serializer.is_valid():
   serializer.save()    # will INSERT or UPDATE your validated data
0
Sławomir Lenart