Skip to main content

A utility to improve and optimise read operations(querying and serialization of data) for Django Rest Framework based applications

Project description

A utility to improve and optimise read operations(querying and serialization of data) for Django Rest Framework based applications

Official version support:

  • Django >=1.11

  • Supported REST Framework versions >= 3.6.4

  • Python >= 3.6

Capabilities

  • Gives the ability to dynamically select required fields to be serialized

    • We can specify required fields to be serialized for a GET request via query params

    • This also reduces response size comparatively as we pick only necessary fields

    • Ability to pick required fields through all kinds of nested relationships(many2one, many2many, reverse_lookups)

  • Improves querying and reduces overall I/O load by a very good factor

    • reduces overall number of queries required to serve a generic GET Request by a rest_framework.viewsets.ModelViewSet

  • Plug and Play

    • Simple API with minimal configurations

What it provides

This package provides following mixins:

  • DynamicReadSerializerMixin

    • provides an API on top of ModelSerializer to provide required fields to be serialized(via kwargs)

    • following kwargs can be passed to a model serializer inheriting this mixin

      • filter_fields : list of serializer field names which should be allowed for serialization

      • omit_fields : list of serializer field names which should not be allowed for serialization

    • DynamicReadSerializerMixin.optimize_queryset : a utility to return a optimized queryset by performing necessary select_related and prefetch_related based on fields and omit, below are the arguments to be passed

      • filter_fields : list of serializer field names which should be allowed for serialization

      • omit_fields : list of serializer field names which should not be allowed for serialization

      • queryset : input queryset object

  • DynamicReadViewMixin

    • provides support on top of rest_framework.viewsets.ModelViewSet to pick required fields to be serialized via query params of a GET request, these required fields are internally forwarded to DynamicReadSerializerMixin

    • optimize_queryset : static boolean attribute which decides whether to perform queryset optimization steps via DynamicReadSerializerMixin.optimize_queryset

    • following query params can be passed for any GET request which is served by a model viewset inheriting this mixin:

      • fields : serializer field names as comma seperated values which should be considered for serialization

      • omit : serializer field names as comma seperated values which should not be considered for serialization

Installing

pip install drf-dynamic-read

Usage

Example Entity Relationship:

from django.db import models

class User(models.Model)
    username = models.CharField()


class EventType(models.Model)
    name = models.CharField()
    created_by = models.ForeignKey(User)


class EventCause(models.Model)
    name = models.CharField()
    created_by = models.ForeignKey(User)


class Event(models.Model):
    type = models.ForeignKey(EventType)
    causes = models.ManyToManyField(EventCause)
    owner = models.OneToOneField(User)

Example serializers for above ER:

from rest_framework import serializers
from dynamic_read.serializers import DynamicReadSerializerMixin


class UserSerializer(DynamicReadSerializerMixin, serializers.ModelSerializer):
    class Meta:
        model = models.User
        fields = "__all__"


class EventTypeSerializer(DynamicReadSerializerMixin, serializers.ModelSerializer):
    created_by_id = serializers.PrimaryKeyRelatedField(
        queryset=EventType.objects.all(), write_only=True, source="created_by",
    )
    created_by = UserSerializer(read_only=True)

    class Meta:
        model = EventType
        fields = "__all__"


class EventCauseSerializer(DynamicReadSerializerMixin, serializers.ModelSerializer):
    created_by_id = serializers.PrimaryKeyRelatedField(
        queryset=EventCause.objects.all(), write_only=True, source="created_by",
    )
    created_by = UserSerializer(read_only=True)

    class Meta:
        model = EventCause
        fields = "__all__"


class EventSerializer(DynamicReadSerializerMixin, serializers.ModelSerializer):
    type_id = serializers.PrimaryKeyRelatedField(
        queryset=EventType.objects.all(), write_only=True, source="type",
    )
    cause_ids = serializers.PrimaryKeyRelatedField(
        queryset=EventCause.objects.all(), write_only=True, source="cause", many=True
    )
    type = EventTypeSerializer(read_only=True)
    causes = EventCauseSerializer(read_only=True, many=True)
    created_by = UserSerializer(read_only=True)

    class Meta:
        model = Event
        fields = "__all__"

Example views for above ER:

from dynamic_read.views import DynamicReadBaseViewMixin
from rest_framework import viewsets
from rest_framework.routers import DefaultRouter

class EventModelViewSet(viewsets.ModelViewSet, DynamicReadBaseViewMixin):
    queryset = Event.objects.all()
    serializer_class = EventSerializer


router = DefaultRouter()
router.register("/api/event_basic/", EventModelViewSet)

A regular request returns all fields:

GET /api/event_basic/

Response:

[
  {
    "id": 1,
    "type": {
      "id": 2,
      "name": "Type2",
      "created_by": {
        "id": 1,
        "username": "user1"
      }
    },
    "cause": [
      {
        "id": 1,
        "name": "Cause1",
        "created_by": {
          "id": 1,
          "username": "user1"
        }
      },
      {
        "id": 2,
        "name": "Cause2",
        "created_by": {
          "id": 2,
          "username": "user2"
        }
      }
    ],
    "created_by": {
      "id": 2,
      "username": "user2"
    }
  },
]

A GET request with the fields parameter returns only a subset of the fields:

GET /api/event_basic/?fields=id,type

Response:

[
  {
    "id": 1,
    "type": {
      "id": 2,
      "name": "Type2",
      "created_by": {
        "id": 1,
        "username": "user1"
      }
    }
  },
  {
    "id": 2,
    "type": {
      "id": 1,
      "name": "Type1",
      "created_by": {
        "id": 1,
        "username": "user1"
      }
    }
  }
]

fields parameter can spawn through the relationships also:

GET /api/event_basic/?fields=id,type__name,cause__name,created_by__username

Response:

[
  {
    "id": 1,
    "type": {
      "name": "Type2"
    },
    "cause": [
      {
        "name": "Cause1"
      },
      {
        "name": "Cause2"
      }
    ],
    "created_by": {
      "username": "user2"
    }
  },
]

A GET request with the omit parameter excludes specified fields(can also spawn through relationships just like the above example for fields).

GET /api/event_basic/?omit=type,cause__created_by,created_by__id

Response:

[
  {
    "id": 1,
    "cause": [
      {
        "id": 1,
        "name": "Cause1",
      },
      {
        "id": 2,
        "name": "Cause2",
      }
    ],
    "created_by": {
      "username": "user2"
    }
  },
]

All the above examples work in the same mechanism for detail routes

Query Optimization

Now first let’s consider this general request which returns all the fields: GET /api/event_basic/

Total number of queries would be: 51

  • 1 (Base query to return all the event objects)

  • 10 x 1 (fetch type for an event)

  • 10 x 1 (fetch created_by for an each type)

  • 10 x 1 (fetch all causes for an event)

  • 10 x 1 (fetch created_by for an event cause)

  • 10 x 1 (fetch owner for an event)

Now let’s define a new view in views.py:

from dynamic_read.views import DynamicReadViewMixin
from rest_framework import viewsets
from rest_framework.routers import DefaultRouter

class EventModelViewSet(DynamicReadViewMixin, viewsets.ModelViewSet):
    queryset = Event.objects.all()
    serializer_class = EventSerializer


class EventOptimizedModelViewSet(DynamicReadViewMixin, viewsets.ModelViewSet)
    optimize_queryset = True
    queryset = Event.objects.all()
    serializer_class = EventSerializer

router = DefaultRouter()
router.register("/api/event_basic/", EventModelViewSet)
router.register("/api/event_enhanced/", EventOptimizedModelViewSet)

Now let’s try the optimized version: GET /api/event_enhanced/

Total number of queries would be: 3

  • .select_related("type", "owner__created_by")

    • 1 (Query which gets all events inner joined with event types(inner joined with users), users)

  • .prefetch_related("causes__created_by")

    • 1 (Query to get all required event causes separately)

    • 1 (Query to get all users(created_by) for event causes)

Now first let’s consider the above example with fields: GET /api/event_enhanced/?fields=type__name,owner__created_by

Total number of queries would be: 1

  • .select_related("type", "owner__created_by")

    • 1 (Query which gets all events inner joined with event types, users)

Testing

Yet to write :)

Planned features

  • API aliasing, single view serving extended url patterns, each url pattern is an alias mapped to specific fields,omit values

  • Restricting the scope of fields,omit w.r.t user defined permissions per API

Credits

  • This implementation is inspired from drf-dynamic-fields by dbrgn

  • Thanks to Rishab Jain for implementing caching in evaluation of select_related, prefetch_related for a QuerySet w.r.t fields, omit

  • Thanks to Martin Garrix for his amazing music, sourcing all the necessary dopamine

License

MIT license, see LICENSE file.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

drf_dynamic_read-0.1.1.tar.gz (10.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

drf_dynamic_read-0.1.1-py3-none-any.whl (9.3 kB view details)

Uploaded Python 3

File details

Details for the file drf_dynamic_read-0.1.1.tar.gz.

File metadata

  • Download URL: drf_dynamic_read-0.1.1.tar.gz
  • Upload date:
  • Size: 10.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.2 importlib_metadata/4.8.1 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.8.11

File hashes

Hashes for drf_dynamic_read-0.1.1.tar.gz
Algorithm Hash digest
SHA256 59b647c25aff4d2decc9c1e603777f4f7052e1d460d3895180ffc573975fc896
MD5 2751ce00939ecc609f9563ca2d917957
BLAKE2b-256 ddc45a673f094f90a65278ef7f3978ae0ed58c8cfbe3de968694eef3b2bd6b38

See more details on using hashes here.

File details

Details for the file drf_dynamic_read-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: drf_dynamic_read-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 9.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.2 importlib_metadata/4.8.1 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.8.11

File hashes

Hashes for drf_dynamic_read-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8bce2cdddc6c6cb964cc156ec02cafb87a2d225e05200227ace497d13331adca
MD5 72221edc6d44f27988453ec799c8fe7c
BLAKE2b-256 750d9559387e836ae1e4396e52b7237ed4933b9d1df464ea22f7778ac8d7f3a7

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page