Django Rest Framework POST Update if existing or create
Asked Answered
C

10

58

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 Question.

On the frontend I used to use POST method to create an answer sent to api/answers, and PUT method to update sent to e.g. api/answers/24

But I want to handle it on the server side. I will only send a POST method to api/answers and DRF will check based on answer_id or question_id (since it is one to one) if the object exists. If it does, it will update the existing one, and if it doesn't, it will create a new answer.

I couldn't figure out where I should implement it. Should I override create() in serializer or in ViewSet or something else?

Here are my model, serializer, and view:

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")
Cowans answered 15/6, 2016 at 10:48 Comment(4)
Won't POST edit the object if there is one already (provided the id is mentioned in the URL)?. From the link: "It's quite possible, valid and even preferred in some occasions, to use PUT to create resources, or use POST to update resources".Jurisdiction
No the article says, if you provide the id in the url, use PUT else use POST. So I want to use POST. But I want it to update not trying to create if there is already that instance. And I want it to be updated partially, so there is also that.Cowans
Hm - I must have interpreted that incorrectly. I see from the article's comments that I'm not alone :) Maybe this previous SO answer can help you along then?Jurisdiction
Thanks but that answer says the contrary: "Update: Can only be performed with PUT in the following way:..."Cowans
B
63

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.

Behm answered 13/4, 2017 at 13:10 Comment(2)
the only problem that it returns HTTP_201_CREATED on update :(Rebarbative
I think this is going into the wrong direction. Update_or_create should be done on the view level instead on the serializer level.Tiler
A
17

Answer posted by @Nirri helped me as well, but I've found a more elegant solution using Django's 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 the Answer to that Question does not exist, it will be created, otherwise it is returned as-is by the 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.

Alethiaaletta answered 20/12, 2016 at 11:46 Comment(2)
As noted in the answer by K Moe this doesn't update the object with the new values passed. https://mcmap.net/q/329963/-django-rest-framework-post-update-if-existing-or-createAlopecia
@Inti: Sure it doesn't. It follows more traditional REST-approach and serves as a reference for more clean solution using shortcuts that are part of API I've posted a link to. Of course, I will update the answer to avoid misunderstanding.Alethiaaletta
K
9

I would use the serializers' create method.

In it you could check if the question (with the ID of it you provide in the 'question' primary key related field) already has an answer, and if it does, fetch the object and update it, otherwise create a new one.

So the first option would go something like:

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

Second option would be to check if the answer with the answer id exists.

Answer ID's wouldn't show up in the validated data of post requests, unless you used a sort of workaround and manually defined them as read_only = false fields:

id = serializers.IntegerField(read_only=False)

But you should however rethink this through, There's a good reason the PUT method and the POST methods exist as separate entities, and you should separate the requests on the frontend.

Karolynkaron answered 15/6, 2016 at 20:14 Comment(1)
Thank you so much. First option would be sufficient. The reason I want to separate them is this: If there is an answer I call UpdateAnswer else CreateAnswer on the front -it is in React btw. So let's imagine you and I opened the website in different browsers. There is an unanswered question, and you clicked "YES" button and and sent data so the server will create the object. I did not refresh the browser and clicked "YES" or "NO" so it will send another create request which will fail. So I have to handle it at the server side.That was why.Cowans
F
8

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

This simulates conditions to update passing the id from request.data to kwargs, so if the instance doesn't exist, the UpdateModelMixin.update() raises an Http404 exception that is caught by the except block and calls 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)
Faun answered 28/4, 2020 at 21:44 Comment(2)
Looking at modelviewset, it already contains the UpdateModelMixin. So why would you pass the ModelViewSet? Could't you just pass UpdateModelMixin and CreateModelMixin?Ers
This should really be the accepted answer, this is a much better approach than needing to specify every parameter within the update_or_create() method. Thanks for this solution!Ignazio
M
3

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')
Meadow answered 6/10, 2019 at 2:43 Comment(1)
Because of the multiple inheritance you need to change the super call to: return super(serializers.ModelSerializer, self).is_valid(). This way it worked for me.Zealand
M
2

Also:

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

if serializer.is_valid():
   serializer.save()    # will INSERT or UPDATE your validated data
Medicable answered 8/8, 2018 at 12:55 Comment(0)
C
1

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
Corrinecorrinne answered 20/9, 2018 at 8:34 Comment(0)
R
1

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']
Rebarbative answered 18/10, 2020 at 13:20 Comment(0)
E
0

If you use to_field in models.ForeignKey(eg: task_id), you need add lookup_field = 'task_id' as blow.

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

Embree answered 10/2, 2023 at 9:8 Comment(0)
P
0
class ApplicationSerializer(ModelSerializer):
    user = serializers.PrimaryKeyRelatedField(
        many=False, queryset=User.objects.all()
    )

class Meta:
    model = Application
    fields = ['id', 'user', 'job', 'status', 'is_pending', 'is_accept', 'is_reject']
Pagano answered 17/12, 2023 at 6:34 Comment(3)
PrimaryKeyRelatedField is there you can try to use that field alsoPagano
Hi ADISH CT, could you please check my edit? I guess there was a formatting issue in the code, right? And could you add some description of your solution?Broadbill
Thank you for your interest in contributing to the Stack Overflow community. This question already has quite a few answers—including one that has been extensively validated by the community. Are you certain your approach hasn’t been given previously? If so, it would be useful to explain how your approach is different, under what circumstances your approach might be preferred, and/or why you think the previous answers aren’t sufficient. Can you kindly edit your answer to offer an explanation?Hairball

© 2022 - 2024 — McMap. All rights reserved.