3 Tips to Make Your Django Proxy Models Journey Easier

Reducing barriers to enhance the adaption of Django Proxy Models

Vlad Ogir
Django Unleashed
4 min readFeb 29, 2024

--

Generated by Microsoft Copilot
Generated by Microsoft Copilot

I recently posted about state management using proxy models. After receiving some questions, I decided to create a follow-up article. This article aims to show how to use proxy models more effectively and reduce complexity further. (The previous article can be found here.)

In this article, I will provide tips on the following three things:

  • How to make automatic resolution of proxies easier.
  • Extra insights into how to transition states.
  • How to set up a map of states to avoid cyclical dependency.

Sounds interesting? Well, let’s get started!

Tip 1: Simplifying Proxy Model Resolver

When using QuerySet with proxies, you get the parent model instead of the proxy when you query the database. Because of this, we need to keep converting the parent model to the proxy equivalent. (It’s not a great developer experience.)

The example below demonstrates this issue:

>>> from blog.models import Post
>>> test = Post.objects.all()

>>> test
<QuerySet [<Post: Post object (1)>, <Post: Post object (2)>]>

>>> test.first().resolve_proxy_model()
<DraftPost: DraftPost object (1)>

One way to simplify this is by adding a resolver within the __init__ method in the parent model, as shown below:

class Post(models.Model):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.resolve_proxy_model()

This approach automatically resolves the model to a relevant proxy every time we create a model. This works with both QuerySet and stand-alone model creation.

>>> from blog.factories import ReadyForReviewPostFactory
>>> ReadyForReviewPostFactory.build()
<ReadyForReviewPost: ReadyForReviewPost object (None)>

>>> from blog.models import Post
>>> test = Post.objects.all()
>>> test
<QuerySet [<DraftPost: DraftPost object (1)>, <PublishedPost: PublishedPost object (2)>]>

One thing to note is when calling all(), we get a list of results. But should we chain any further queries (such as first()) the clone() method is called. This method resets the query to a basic state. The suggested approach also covers this scenario. In the example below I am still getting the proxy model instead of the original post model.

>>> post = test.last()
>>> post
<PublishedPost: PublishedPost object (2)>

Tip 2: Resolve Proxies after Performing Transitions

When performing transitions the automatic resolver will not swap underlining proxy class. It will be up to us to do it. To accomplish this we need to call the resolver after the state transition.

Below is an example of a proxy model with the transition method complete_state:

class DraftPost(Post):
class Meta:
proxy = True

def complete_state(self):
self.status = STATUS_READY_FOR_REVIEW
self.save()
self.resolve_proxy_model()

return self # optionally return `self` to chain method calls

This is an example of usage:

>>> post
<DraftPost: DraftPost object (4)>
>>> post.complete_state()
>>> post
<ReadyForReviewPost: ReadyForReviewPost object (4)>

Tip 3: Tackling cyclical dependency when creating a Map of States

The mapper is there to define which proxy model represents which state. Since the parent model uses the mapper, the mapper needs to exist before the parent model. This relationship may lead to cyclical dependency. (Static analysis tools would flag this issue.) I’ve illustrated this issue in the code below:

MAPPER = {
"state": Proxy # The Proxy is yet to be defined
}

class Post(model):
def resolve_proxy_model(self):
MAPPER.get()...

class Proxy(model):
...

There are various solutions to the cyclical dependency. One way is by defining everything in a single file. The file will be fully loaded before being executed. So, we can define an empty mapper dictionary and populate it as we add proxies through the code. Below is an example code:

MAPPER = {}

class Post():


class MyProxy():


MAPPER["my-state"] = MyProxy

Projects, where models are split between multiple files, may benefit from a config file approach. (Learn more about model organisation here: “Organizing models in a package”.) For this, we can use a singleton class to pass the state across the application. Below is an example of a singleton class:

class Mapper:
state = {}

def __new__(cls):
if not hasattr(cls, 'instance'):
cls.instance = super(Mapper, cls).__new__(cls)
return cls.instance

Once we have the class, we need to populate it with data. There are various places where this can be done:

  • We can add it to the __init__.py file within the models' root folder.
  • Within the AppConfig's ready method.
  • If you keep proxies in a separate folder, we can also do it within the __init__.py file, where proxies are kept.

Below is an example of applying this within the proxies folder. Within the /blog/models/post_proxies/__init__.py, I have code that maps statuses to the relevant proxies.

from blog.constants import STATUS_DRAFT, STATUS_PUBLISHED, STATUS_READY_FOR_REVIEW
from blog.state import Mapper
from .post_proxies.draft_post import DraftPost
from .post_proxies.published_post import PublishedPost
from .post_proxies.ready_for_review_post import ReadyForReviewPost

Mapper.state = {
STATUS_DRAFT: DraftPost,
STATUS_READY_FOR_REVIEW: ReadyForReviewPost,
STATUS_PUBLISHED: PublishedPost,
}

Conclusion

  1. The automatic resolver doesn’t have to be difficult; it’s a one-liner!
  2. When transitioning to other states, remember to call the resolver again. (This is something that can be abstracted to simplify use.)
  3. When mapping states to proxies, we need to account for circular dependencies. Make sure that the mapper is populated before it is used.

Having a useful tool is great, but it’s of no use if others don’t adopt it or if it causes more issues down the line. By reducing barriers that stand in the way we are making a tool more accessible. As a result, we can benefit from the tool and focus more on objectives that add actual business value.

Hopefully, this article has given you greater insight into how you can implement proxies in your project today!

p.s. source code for example proxy model implementation mentioned in this article.

I’d love to hear your thoughts! Please share your questions and insights in the comments below or contact me directly.

Want more articles like this? Subscribe to my email list for exclusive content and updates on my latest work.

--

--

Vlad Ogir
Django Unleashed

Staff software engineer with passion for software delivery, architecture and design.