Django is a very popular tool for building websites with Python. It’s great for quickly making simple websites, but as projects get bigger, it can become tricky to manage and support them.
To deal with these challenges, developers use different ways of organizing their projects and try to apply ‘Clean Architecture’ approaches, patterns to Django, DRF projects.
To learn more about using ‘Clean Architecture’ with Django, you can read the article.
A key idea of most patterns are to keep different parts of the project decoupling, independent from each other to give them flexibility to scale and change.
One of these patterns is called the ‘Data Transfer Objects Pattern’.
This idea was first explained by Martin Fowler in his book. Basically, it means using special objects to move data between different parts of the project in order to reduce the number of methods calls.
When we use this approach in our project, we also get another benefit. It is the encapsulation of the serialization’s logic (the mechanism that translates the object structure and data to a specific format that can be stored and transferred). It provides a single point of change in the serialization nuances. It also decouples the domain models from the presentation layer, allowing both to change independently.
DTO pattern is often used with Repository pattern that isolates the data layer from the rest of the app.
The repository will be complete tie and depend from current ORM implementation but to make independent other layers it will return the DTO, which does not dependent from the framework implementation.
# dto.py
@dataclass
class InstanceDTO:
id: int
name: str
# repository.py
def get_obj_by_id(self, instance_id: int) -> Union[CategoryDTO, None]:
django_orm_obj = get_object_or_None(Model, pk=instance_id)
instance_dto = InstanceDTO(
id=django_orm_obj.pk,
name=django_orm_obj.name
)
return instance_dto
Looks simple, but what if your django_orm_obj that returns from the database is more complex?
For example, has related models or recursive relation.
# repository.py
def get_obj_by_id(self, instance_id: int) -> Union[CategoryDTO, None]:
django_orm_obj = get_object_or_None(
Model.objects.select_related('ModelA')
.prefetch_related('ModelB', 'ModelC__ModelD'), pk=instance_id
)
...
To make DTO from this django_orm_obj, we are required to build a separate function to map it to DTO. And if there are a bunch of this queries their mapping turns into nightmare.
But I have a good news for you, there is a Python package that make all of this instead of you. The AutoDataclass is a simple package that helps easy to map data into DTO for transporting that data between system layers. The package uses specified Dataclass structure for retrieving data and creating DTO.
Now, I want to share with you of how to use this package in your project.
Installation
pip install auto_dataclass
Usage
- Simple models relations
# models.py
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=128)
description = models.TextField(max_length=1000)
class Photo(models.Model):
product = models.ForeignKey(Product, related_name="photos",
on_delete=models.CASCADE)
image = models.ImageField(blank=True)
Define your Dataclasses that describe retrieved data structure from DB.
# dto.py
@dataclass(frozen=True)
class PhotoDataclass:
id: int
image: str
@dataclass(frozen=True)
class ProductDataclass:
id: int
name: str
description: str
photos: List[ProductDataclass] = field(default_factory=list)
Create Converter
instance and call to_dto
method with passed data from the query and previously defined Dataclass.
# repository.py
from auto_dataclass.dj_model_to_dataclass import FromOrmToDataclass
from dto import ProductDataclass
from models import Product
# Creating Converter instance
converter = FromOrmToDataclass()
def get_product(product_id: int) -> ProductDataclass:
product_model_instance = Product.objects \
.prefetch_related('photos') \
.get(pk=product_id)
# Converting Django model object from the query to DTO.
retrun converter.to_dto(product_model_instance, ProductDataclass)
- Recursive Django model relation
If your data has a recursive relation you can also map them with the same way.
# models.py
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=128)
parent = models.ForeignKey(
"Category", related_name="sub_categories", null=True, blank=True, on_delete=models.CASCADE
)
# dto.py
@dataclass
class CategoriesDTO:
id: int
name: str
sub_categories: List['CategoriesDTO'] = field(default_factory=list)
# repository.py
from itertools import repeat
from auto_dataclass.dj_model_to_dataclass import FromOrmToDataclass
from models import Category
from dto import CategoriesDTO
converter = FromOrmToDataclass()
def get_categories(self) -> Iterable[CategoriesDTO]:
category_model_instances = Category.objects.filter(parent__isnull=True)
return map(converter.to_dto, category_model_instances, repeat(CategoriesDTO))
More example you can find in repo AutoDataclass.
Summary
DTO is a simple but powerfull pattern that allows to decoupling bussiness logic from the framework.
In order to quickly and easy implement it in your app, you may use AutoDataclass package, which will help to map Django ORM object to DTO.
Also, I would recommend to use djangorestframework-dataclasses.
This package helps you avoid duplicate the fields in DTO and Serializer definitions. Serializer will be constract automatically from DTO schema.