In this article we dive a bit into Django’s way of data modelling and that of DDD’s notion of a repository. An example implementation can be found on my github here.
The design of data modelling in Django can be interpreted as an application of the ‘Active Record Pattern’. A model in Django is a class with field attributes, model methods, a reference to a manager (see below) and serves partly to interact with the underlying database.
from django.db import models class MyModel(models.Model): # define field attributes name = models.CharField(max_length=256) # custom manager objects = MyManager() # model methods def hey_a_model_method(self): return self.name + "!"
Models can be saved, deleted and updated. Another way to communicate with the underlying database is through a manager. Managers are coordinators for collections of entities, as the Django documentation formulates it, in a table-wide manner.
from django.db import models class MyManager(models.Manager): def get_queryset(self): return super().get_queryset().values('name')
Then one can call MyModel.objects.all() to retrieve certain data. (Question: How does the manager know how to look up data? Answer: add_class of ModelBase calls contribute_to_class for all its attributes, including a manager.)
Managers give access to entities of a model and it is sort of the standard way to retrieve entities or a specific entity (unless you create one directly). The underlying data structure of a manager is a QuerySet (https://docs.djangoproject.com/en/3.0/ref/models/querysets/#all) . Also QuerySets can be subclassed and extended with domain specific, the custom QuerySet can be used by overriding the “get_queryset” method (commented out in the above code snippet). A manager possesses all methods of the underlying QuerySet due to its class method “_get_queryset_methods”.
Now the question remains, with Model, Manager and QuerySet at our disposal, how do this data quering integrate with Django and what is then a good strategy to mix in DDD?
The data querying of the previous section is normally initiated by views. For plain vanilla Django, subclasses of GenericView and the likes can be used that specify a model, whence implicitly define a QuerySet to be used for the view, or alternatively, one can set a QuerySet directly.
from django.views.generic.edit import CreateView class MyView(CreateView): model = MyModel # ..
For views derived from the Django Rest Framework (DRF), a similar approach is used.
What is a repository?
Now that we briefly summarised data handling in Django, what does DDD say about a repository? A repository is an object that provides a ‘globally accessible’ interface for adding and removing of objects, for instance, aggregates. They persist objects. How many repositories are there? It depends what you need, you can have one for each aggregate for example. Repositories should contain no business logic.
Redirecting Django to a repository
We saw that in Django, the entry points to storing and retrieving data are supplied by both the model and the manager. Both the model and manager can create objects: the model a single object (as well as the manager which delegates this in the background to the model) and the manager a bulk of objects. This two way part is contrary to the DDD philosophy to have one repository that handles access to the database.
By using mixins, or subclassing plus overriding methods, we can redirect the flow to save data to always use a save method in the manager. A manager is considered to be a ‘repository’ once all creation, updating and deleting takes place through the manager.
An example redirection
This section illustrates how saving objects is redirected from the save method of the model to a save method of the manager or repository. There are multiple possibilities to implement redirection:
- Change the model to not save/update/delete
- Adapt behaviour of serializers to use the repository
- Adapt views using hooks around standard serializer behaviour
- Use Django signals with pre/post save hooks etc
Here I opted for the first option. The downside of the second option is that this would only work for objects changed through a serializer. The third option can be done by example by using the perform_* hooks that DRF supplies. The fourth option is not really a redirect by a signal and no full control is obtained for saving an object (signals are also asynchronous).
First change modify the model and manager to inherit from new base classes:
from django.db import models from ddd import DDDModel, DDDManager class MyManager(DDDManager): pass class MyModel(DDDModel): # define field attributes name = models.CharField(max_length=256) # custom manager names = MyManager() repository = MyRepository
How the calls of the model and manager are intercepted just by inheritance can be read in more detail from here . Now the repository will look like the following:
from utils.singleton import Singleton from ddd import repository, DDDRepository @repository class MyRepository(DDDRepository): def __init__(self, model): self.model = model @transaction.atomic def bulk_create(self, objs): # some bulk creation code pass @transaction.atomic def create(self, obj): # some preflight code here obj.save() # some other code there @transaction.atomic def delete(self, obj): # some other code obj.delete() @transaction.atomic def update(self, obj): # some code pass
The upshot is that this class, a singleton, is a central one place where database access can be controlled, and for example additional handlers or ACL logic can be hooked in.