Django Proxy Models: The Secret Weapon for Cleaner, Simpler Code
Unlock cleaner, more maintainable models with this powerful technique
We store data to record the state: A password change, an order made, an order delivered and etc. Such things can be seen as stand-alone events or, in many cases, they are part of a step in a process. And, it's common to see those things be chucked into a single table.
All those columns can result in models that bloat to 1000s of lines. When looking at a model like that, you may have an urge to group all columns and methods to make sense of what is going on. It's tiring enough doing it once, but it becomes a bigger issue when you and others have to go through the same process each time.
In situations like this, proxy models can become invaluable! They can enable you to split columns in models into targeted sections.
In this article, I will:
- Share the basics of what proxy models are
- Why should we use them
- Give practical examples
Throughout the article, I will use an imaginary `Post` model that got too big. The focus will be to partition the `Post` model by state into proxy models.
What are proxy models?
Proxy models are there to "decorate" existing Django models. This is very similar to what Python's decorators do or the Decorator pattern, except we apply decoration on a model.
Proxies are also similar to model abstraction. The Proxy model inherits the data and base methods from the parent model, but _without affecting the underlying table_ that the model represents. This removes the ability to make any changes to the underlying table structure via proxy models.
You can learn more about proxies in Django's docs.
Why use proxy models?
Models can easily get to the point where they become bloated quickly, and this can lead to difficulties in understanding the purpose of the model and when methods should be used. In the end, the code can end up being overcomplicated.
With proxies, we can resolve these problems by simply extracting code specific to the state into the proxy model. As a result, you end up with smaller, targeted models that are easier to understand and to maintain.
Example Usage
Let's imagine we are working on a blog, and as part of the process, all posts have to go through a review.
When a post is created, we don't need to know things like the performance of a post or the review process. These things are only applicable once the post is in a certain state.
Now, imagine we have a bloated `Post` model. With the help of proxies, we can create classes that encapsulate specific states and have state-specific functionality. In the example below, I demonstrate exactly that by encapsulating `Draft` and `Published` states:
from django.db import models
class Post(models.Model):
subject = models.CharField(max_length=30)
content = models.TextField()
class DraftPost(Post):
# custom defaults for class properties can be defined
type = BLOG_TYPE_DRAFT
class Meta:
proxy = True
def complete_state(self):
self.status = STATUS_READY_FOR_REVIEW
self.save()
class PublishedBlog(Post):
# we can override base object with a custom manager
objects = PublishedBlogManager()
type = BLOG_TYPE_PUBLISHED
class Meta:
proxy = True
# we can have our own custom methods or override parent's methods
def performance(self):
...
By doing this, we have:
✅ Improved visibility of states
✅ Proxy classes can now be used as type hints
✅ State-specific methods are within their own class
✅ We have definition of how to transition to the next state
✅ We limit scope of QuerySet of each proxy using custom managers
Now it's a lot clearer and safer to work with data.
Proxy model resolver: turning Model into Proxy equivalent
One downside to using proxies is that Django doesn't auto resolve the parent model to a proxy model when retrieving data from a database. So when using `DraftPost` proxy to retrieve data, Django will return `Post` models back.
To resolve this problem, we can create a function that will turn `Post` models into a correct proxy model. To achieve this, we need a column (or a combination of columns) that determines the current state. For simplicity, I will use the `status` column for this purpose.
The code below demonstrates how this can be accomplished by:
Create a mapper that maps status to a proxy class.
Then create a method that will turn a class into a proxy representation, based on the current status.
from django.db import models
class Post(models.Model):
# Mapper that maps status to a proxy class
POST_STATUS_FLOW = {
STATUS_DRAFT: DraftPost,
...
}
# Method that turns class into a relevant proxy class
def resolve_proxy_model(self) -> Post:
proxy_class = MAPPER.get(self.status)
self.__class__ = proxy_class
return self
...
# usage
proxy_model = post.resolve_proxy_model()
💡 You can also automatically resolve the proxy model without manually calling the resolver method.
Conclusion
Proxy models can be a very useful tool to separate concerns. It allows you to encapsulate all the relevant logic for the state in one single class. Afterwards, you can create custom methods, query managers and transition-related requirements for that single state.
Additionally, this technique will improve the visibility of different states and make it easier for other engineers to understand the intended flow of the whole process.
But, proxies shouldn't be seen as a solution for a lack of database design. Since well-defined data would avoid the need for proxies to begin with!
So, why wait? Pick some models and start refactoring today!
---
I'd love to hear your thoughts! Please share your questions and insights in the comments below or contact me directly.