Skip to content

Django DRF Cheatsheet

Shaung Cheng edited this page Dec 5, 2020 · 8 revisions

Several more complicated topics described as per-section below.

Updating nested field (nested serializer)

How does DRF handle update request like PATCH/PUT when the parent object has relational, nested fields? This also implies that the parent object's serializer contains other object's serializer, which is also what we mean by "nested serializer".

By looking at several examples, like on SO, or this post, there seems no way DRF is doing update for us. The latter post even states:

Nested serializers by default don't support create and update

So we can probably assume that nested serializer supports GET, where it will return the full parent object with all children, relational field object embedded. But, when it's a PATCH/PUT that involves update, the parent serializer does not update them out of the box (not sure, however).

The dominant way people is doing is, pop out the validated client data, use it, and update the nested fields manually. An important part like this post mentioned is to pop out the nested field data, so that DRF does not process on them while we still want to reuse DRF for the rest of the updates (like other non-relational fields on parent object)

Lifecycle (control flow) of DRF

Refer & credit to this SO answer for clarifying the control flow.

Step 1: View Dispatch

The viewset calls dispatch, which in turn checks if there's corresponding method on the viewset class. It all works in lower case. e.g., GET method request will find a def get(), which by default is already there. Same for POST.

However for other methods, like PATCH, etc, you'll have to add the method def patch() yourself on the viewset class. Otherwise you'll get 405 method not allowed.

Step 2: View HTTP Method Handler

  • HTTP POST method -> calls create()
  • HTTP PUT method -> calls update()
  • HTTP PATCH method -> you can call partial_update() -> in turns call update(partial=True)

View def update(self, request, *args, **kwargs) -> Response(serializer.data)

DRF does not implement update behavior for ListSerializer, so below only applies to not many serializer case. However, for each steps below, we include what you should do if you want to implement and customize update() behavior for ListSerializer. You'll overwrite def update() on your serializer class, and perhaps checking if request.data is a list, then run your custom update logic.

  1. Gets instance (QuerySet) in db, it simply calls self.get_object().
    • For ListSerializer, you can use self.get_queryset().filter(uuid__in=your_uuid_list) to get the instances.
  2. Create serializer instance - serializer(instance=instance, data=request.data, partial=partial). Partial is only used when serializer is validating. Django's ORM is already doing partial update if attribute not passed over as kwargs in instance.update().
    • For ListSerializer, do serializer(instance=instances, data=request.data, partial=partial, many=True)
  3. Run validation - call serializer.is_valid(raise_exception=True), then jump to Step 3 below.
    • For ListSerializer, no change needed
  4. Ready to commit to database - run self.perform_update(serializer), jump to Step 4 below.
    • For ListSerializer, no change needed
  5. Response data back to client - return Response(serializer.data)
    • For ListSerializer, no change needed

Step 3: Validation

Calling serializer.is_valid() will call the methods below on the serializer class. To customize, add these methods and customize them on the serializer class.

def validate(self, parent_object) -> parent_object

To setup validation error, simply raise the exception serializers.ValidationError.

def validate_${field_name}(self, attrs) -> attrs

Write your own Validators

Only do this if you want to reuse validation logic.

Step 4: Commit to Database (Part 1) (Not applicable for GET method)

If you come from update operations (PUT/PATCH), will call perform_update() on viewset class.

If from POST request, will call perform_create() on viewset class instead.

View perform_update()

  1. (many=False) It simply calls serializer.save(); you can overwrite attributes by passing kwargs e.g., .save(user=enforced_user_object). This in turn calls serializer's create() or update(). Then go to Step 5 below.
    • For ListSerializer , kwargs supplied to .save(...) enforcing attribute overwriting, will be applied to all instances.

Step 5: Commit to Database (Part 2)

If in step 2, we create serializer by instance=..., then it'll automatically call update(); otherwise in step 2 if you pass instance=None or don't pass it, it'll automatically call create() here.

Serializer def update(self, instance/instances, validated_data) -> updated_instance(s)

  1. Loops through validated_data items, assign data onto instance attributes, call instance.save(), and return instance.
    • For ListSerializer, the 1st arg will be a list of instances, so we better use plural instances for arg name. We'll need to loop over each instance and update their attributes. Simply do so by calling updated_instance = self.child.update(instance, validated_data), and return updated_instance at the end. No need to call instance.save() on each instance, because self.child is the singular (many=False) Serializer, and by default def update() on singular Serializer is already calling instance.save().
    • If you want to use bulk_update(updated_instance, writable_fields) which requires Django>2.2, you want to avoid calling instance.save() on each instance. Hence, either don't call self.child.update(...) and set attributes to instances yourself, or call self.child.update(...) but overwrite def update() on singular serializer class such that it does not call instance.save() when data is a list. Then you can pass over updated_instance to bulk_update(). Take a look at this Medium post as an example