DEV Community

Vlad Ogir
Vlad Ogir

Posted on • Edited on • Originally published at vladogir.substack.com

Django Proxy Models: The Secret Weapon for Cleaner, Simpler Code

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.

Sample of what personas may care about during different post-states

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):
      ...
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

💡 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.

Top comments (0)