Django Rest Framework POST Update if existing or create

前端 未结 8 1818
余生分开走
余生分开走 2020-12-13 09:24

I am new to DRF. I read the API docs, maybe it is obvious but I couldn\'t find a handy way to do it.

I have an Answer object which has one-to-one relationship with a

相关标签:
8条回答
  • 2020-12-13 09:48

    A more generic answer, I think this should be in viewset instead of the serializer, because serializer need just serialize, nothing more.

    This simulates conditions to update passing the id from request.data to kwargs, when if the instance doesn't exists, the UpdateModelMixin.update() raises an Http404 exception what is catched by except block and do a 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)
    
    0 讨论(0)
  • 2020-12-13 10:00

    Answer posted by @Nirri helped me as well, but I've found more elegant solution using Django QuerySet API shortcut:

    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
    

    It does exactly the same thing - if Answer to that Question does not exists, it will be created, else - returned as is by question field lookup.

    This shortcut, however, won't update the object. QuerySet API has another method for an update operation, which is called update_or_create and posted in other answer down the thread.

    0 讨论(0)
  • 2020-12-13 10:01

    I tried the serializer solution but it seems exception raised before hitting the serializer function create(self, validated_data). That's because I'm using ModelViewSet (which in turn using class CreatedModelMixin). Further study reveals that exception raised here:

    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
    

    Since I want to keep all features provided by framework, so I prefer capturing the exceptions and route over to update:

    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
    

    My app can always call POST /api/my_model/ with parameters (here, uuid = primary key).

    However, would it be better if we handle this in update function?

        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
    
    0 讨论(0)
  • 2020-12-13 10:01

    This mixin will allow to use create or update in 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
    

    How to use:

    class DatacenterListSerializer(CreateOrUpdateMixin, serializers.ModelSerializer):
        class Meta:
            model = Datacenter
            fields = ['somefield', 'somefield2']
    
    0 讨论(0)
  • 2020-12-13 10:05

    A better and more generalized way to apply this would be to update the ModelSerializer object with a potential instance if it exists. This allows DRF to follow standard protocols and can be abstracted across models easily.

    To keep things generic, start by making an UpdateOrCreate class to be inherited alongside the modelSerializer on instantiation. In this, add the def update_or_create_helper.

    Then inherit the UpdateOrCreate class for each Serializer you want the functionality with and add a simple is_valid def specific to that model.

    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')
    
    0 讨论(0)
  • 2020-12-13 10:06

    Unfortunately your provided and accepted answer does not answer your original question, since it does not update the model. This however is easily achieved by another convenience method: 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
    

    This should create an Answer object in the database if one with question=validated_data['question'] does not exist with the answer taken from validated_data['answer']. If it already exists, django will set its answer attribute to validated_data['answer'].

    As noted by the answer of Nirri, this function should reside inside the serializer. If you use the generic ListCreateView it will call the create function once a post request is sent and generate the corresponding response.

    0 讨论(0)
提交回复
热议问题