DRF create @property decorator in view and use property in serializer
Tags: drf
Published: 2023-11-07 10:34:15

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:**

```bash
# 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)
```
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**

```bash
# 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)
```
As you can see here the time is changing, every time we call `foobar` we get the current time.

**third example**

```bash
# 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)
```
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**

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

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**
```python 
# 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)
```

vs

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

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:

```python 
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
```
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.