6

I am using Django Rest Framework. I want to create a record if it doesn't exist, or update it if it does exist.

What I did:

class MyModelList(generics.ListCreateAPIView):
    queryset = MyModel.objects.all()
    serializer_class = MyModeSerializer
    permission_classes = (permissions.IsAuthenticated,)

    def perform_create(self, serializer):
        my_model, created = MyModel.objects.update_or_create(user_id=self.request.data['user_id'],
            defaults={
                'reg_id': self.request.data['reg_id']
            })

The record is created or updated, but I am getting an error 'OrderedDict' object has no attribute 'pk'. How do you use update_or_create?

Josh Correia
  • 3,807
  • 3
  • 33
  • 50
chrizonline
  • 4,779
  • 17
  • 62
  • 102

3 Answers3

8

Firstly, this is a bad idea, because you're breaking the REST API architectural style, which expects creates via POST and updates via PUT and PATCH.

But assuming you have a good reason for doing this, perform_create is meant to be called after creation to add other stuff that you need to do while adding the model instance. The more relevant thing to do is to override the create method to update the object if necessary.

I would do it this way.

class MyModelList(generics.ListCreateAPIView):
    queryset = MyModel.objects.all()
    serializer_class = MyModeSerializer
    permission_classes = (permissions.IsAuthenticated,)

    def create(self, request, *args, **kwargs):
        mymodel=None
        id=request.data.get("id")
        if id:
            mymodel=self.get_object(id)

        if mymodel:
            return self.update(request, *args, **kwargs)
        else:
            return self.create(request, *args, **kwargs)
Josh Correia
  • 3,807
  • 3
  • 33
  • 50
pragman
  • 1,564
  • 16
  • 19
3

I would agree with this conclusion of a relevant discussion and implement PUT-as-upsert.

Which means that:

def put(…): would call self.upsert(…) the same way def post(…): calls self.create(…) here.

def upsert(…) would call self.perform_upsert(serializer), similar to this.

A tricky point here if you want a partial update; in which case you might also want to sql-UPDATE only changed fields; and other complications.

So, for non-partial update, def perform_upsert(serializer): would call serializer.upsert().

Another complication here if you want nested create, i.e. with relations.

And you probably need to specify which field(s) to use as the key.

The resulting serializer will look like this:

class MyModelSerializer(serializers.ModelSerializer):

    …

    _key_attrs = ('username',)  # the effective primary key, such as auth_user's username.

    def upsert(self):
        assert not self.errors
        validated_data = self.validated_data
        effective_key = {key: validated_data.get(key) for key in self._key_attrs}
        instance, _ = model.objects.update_or_create(defaults=validated_data, **effective_key)
        return instance

and if you do need anything more complicated, then you need to look at the code of update_or_create to correctly manage the transaction (with its for update lock) and to avoid unnecessary database queries.

HoverHell
  • 4,739
  • 3
  • 21
  • 23
2

Thanks @Bitonator. This is my final solution:

class MyModelList(generics.ListCreateAPIView):
    queryset = MyModel.objects.all()
    serializer_class = MyModeSerializer
    permission_classes = (permissions.IsAuthenticated,)

    def create(self, request, *args, **kwargs):
        myMode, created = MyModel.objects.update_or_create(user_id=request.data['user_id'],
                                                           defaults={
                                                             'reg_id': request.data['reg_id']
                                                           })

        # require context={'request': request} because i'm using HyperlinkModelSerializer
        serializer = MyModelSerializer(myModel, data=request.data, context={'request': request})
        if serializer.is_valid():
            serializer.save()

        if created:
            return Response(serializer.data, status.HTTP_201_CREATED)
        else:
            return Response(serializer.data, status.HTTP_200_OK)
chrizonline
  • 4,779
  • 17
  • 62
  • 102
  • Works too. The only issue I see with your code is that it won't honour object-level permissions. See http://www.django-rest-framework.org/api-guide/permissions/#object-level-permissions for more details – pragman Dec 15 '16 at 04:24