DEV Community

DoriDoro
DoriDoro

Posted on

DRF create @property decorator in view and use property in serializer

To decrease the database queries inside of your view you can create an attribute for your view, so called property. With the creation of the attribute of the view you can make a database query once and use in later use of the attribute the cache to perform actions.

The @property function has just self as parameter, no other parameter.

In this example the property/attribute of the view is static. But you can create a property to make this property/attribute dynamic. If you create this property with no condition, then every time the view is called, you will have every time a different value.
Whereas an attribute of the model is a static value, it is every time the same except you change the value in the database.

Examples and visualization of the creation of a property:

first example:

# terminal

>>> class A:
...    foobar = timezone.now()
... 
>>> a = A()
>>> a.foobar
datetime.datetime(2023, 11, 7, 10, 5, 54, 841005, tzinfo=datetime.timezone.utc)
>>> a.foobar
datetime.datetime(2023, 11, 7, 10, 5, 54, 841005, tzinfo=datetime.timezone.utc)
>>> a.foobar
datetime.datetime(2023, 11, 7, 10, 5, 54, 841005, tzinfo=datetime.timezone.utc)
Enter fullscreen mode Exit fullscreen mode

As you can see every time we call a.foobar the time is the same because we stored the time inside the value of foorbar.

second example

# terminal

>>> class A:
...    @property
...    def foobar(self):
...       return timezone.now()
... 
>>> a = A()
>>> a.foobar
datetime.datetime(2023, 11, 7, 10, 9, 39, 384008, tzinfo=datetime.timezone.utc)
>>> a.foobar
datetime.datetime(2023, 11, 7, 10, 9, 48, 224372, tzinfo=datetime.timezone.utc)
>>> a.foobar
datetime.datetime(2023, 11, 7, 10, 9, 56, 199316, tzinfo=datetime.timezone.utc)
Enter fullscreen mode Exit fullscreen mode

As you can see here the time is changing, every time we call foobar we get the current time.

third example

# terminal

>>> class A:
...    _foobar = None
...    @property
...    def foobar(self):
...       if self._foobar is None:
...          self._foobar = timezone.now()
...       return self._foobar
... 
>>> a = A()
>>> a.foobar
datetime.datetime(2023, 11, 7, 10, 23, 1, 962058, tzinfo=datetime.timezone.utc)
>>> a.foobar
datetime.datetime(2023, 11, 7, 10, 23, 1, 962058, tzinfo=datetime.timezone.utc)
>>> a.foobar
datetime.datetime(2023, 11, 7, 10, 23, 1, 962058, tzinfo=datetime.timezone.utc)
Enter fullscreen mode Exit fullscreen mode

This time the value of the time is stored inside a value in the view. The time is created and stored inside the value _foobar and every time we call the a.foobar we receive the same return value.

An example to show the reduction of database queries in one view:

first example: several database queries

# views.py

class ContributorViewSet(ModelViewSet):

    def get_queryset(self):
        project = get_object_or_404(Project, pk=self.kwargs["project_pk"])

        return project.contributors.all()

    def perform_create(self, serializer):
        project_id = self.kwargs["project_pk"]
        project = (
            Project.objects.filter(pk=project_id)
            .prefetch_related("contributors")
            .first()
        )

       project.contributors
.add(serializer.validated_data["user"])

    def perform_destroy(self, instance):
        project_id = self.kwargs["project_pk"]
        project = get_object_or_404(Project, pk=project_id)

        project.contributors.remove(instance)
Enter fullscreen mode Exit fullscreen mode

In this above example I do in every function (get_queryset, perform_create and perform_destroy) a database query for the project model.

  1. get_queryset: project = get_object_or_404(Project, pk=self.kwargs["project_pk"])
  2. perform_create: project = Project.objects.filter(pk=project_id).prefetch_related("contributors")
  3. perform_destroy: project = get_object_or_404(Project, pk=project_id)

Let's see what a creation of a project property/attribute of the view will change.

second example: creation of the property in view

# views.py

class ContributorViewSet(ModelViewSet):

    # create this variable to avoid unnecessary database queries
    _project = None  

    @property
    def project(self):
        """create an attribute 'project' inside the ContributorViewSet
        this attribute is available in the view and can be called/available in the serializer
        """

        # if the view was never executed before, will make the database query
        #   otherwise _project will have a value and no database query will be performed
        if self._project is None:
            self._project = get_object_or_404(
                Project.objects.all().prefetch_related("contributors"),
                pk=self.kwargs["project_pk"],
            )
        return self._project

    def get_queryset(self):
        return self.project.contributors.all()

    def perform_create(self, serializer):
        self.project.contributors.add(serializer.validated_data["user"])

    def perform_destroy(self, instance):
        self.project.contributors.remove(instance)
Enter fullscreen mode Exit fullscreen mode

vs

# views.py

class ContributorViewSet(ModelViewSet):

    # create this variable to avoid unnecessary database queries
    _project = None  

    @property
    def project(self):
        if not hasattr(self, '_project'):
            self._project = get_object_or_404(
                Project.objects.all().prefetch_related("contributors"),
                pk=self.kwargs["project_pk"],
            )
        return self._project

    def get_queryset(self):
        return self.project.contributors.all()

    def perform_create(self, serializer):
        self.project.contributors.add(serializer.validated_data["user"])

    def perform_destroy(self, instance):
        self.project.contributors.remove(instance)
Enter fullscreen mode Exit fullscreen mode

Now in this two examples in view, there is just one database query for the project and each time, I need to use the project instance I reference to the view attribute self.project. With self.project we can access the attribute of the view.

Even in the serializer of this view we can use the attribute 'project', like this:

serializers.py

class ContributorSerializer(serializers.ModelSerializer):

    user = serializers.IntegerField(write_only=True)

    class Meta:
        model = UserModel
        fields = ["id", "user"]

    def validate_user(self, value):
        if self.context["view"].project.contributors.filter(pk=value).exists():
            raise serializers.ValidationError(
                "This user is already a contributor of this project."
            )

        return user
Enter fullscreen mode Exit fullscreen mode

To use the view attribute 'project' we can access it by self.context['view'].project. The serializer contains all context of the view by this approach.

Top comments (0)