I have tried my hand a bit at using Django the last weeks and it is a web framework, that really is fun to develop in.
However there is one thing that I came to fight with quite a bit. That is building a good multi-tenant aware application in django.
It is to be said, that I wanted a true multi-tenant application, that could serve different data universes from the same running server and the same database, which is a focus that django simply hadn’t been build for yet. Nevertheless, since it is a very transparent and straight forward framework, I actually got it to work quite nicely.
What are those tenants anyway?
In software-design, there is a basic concept, that has been around for a couple of decades now and has been defined only in the most broad sense of the word. Interpretations of the definition vary from a very broad approach, to a very narrow definition.
The basic definition that I can extract from it, is this: “A multi-tenant enabled software can serve different instances of data from one running application.”
For my specific needs, that means, that there can be different versions of my web applications, accessed e.g. through different sub-domains of different URL-prefixes. Those different versions are all supported by the same software and database, but host completely different data-instances, that don’t know anything about each other.
In short, I wanted:
- One django project
- One database
- A ‘key’ model, that serves as the central anchor for a tenant. (In my case, a ‘product’)
- As little knowledge about the multi-tenant situation inside the single django-apps as possible
After a bit of thought about the overall design, I decided, that the tenant that would serve a given request, would be determined by a prefix in the URLs. This means, that I had access to the information which tenant to load at the urls.py module and would need to eigther handle the selection here, or pass it through in some way.
I used the given URLs, to first pick the tenants name, as a named argument to the view, like in the example below:
urlpatterns = patterns('', url(r'^admin/doc/', include('django.contrib.admindocs.urls')), url(r'^admin/', include(admin.site.urls)), url(r'^(?P<product>\w+)/$', 'commons.views.presentation'), url(r'^(?P<product>\w+)/blog/$', include('blog.urls')), )
That is ok, but there are two issues, that I now had to solve: I did not want to need to load the complete product, the url name refered to in every view again; I also didn’t want to be tied down, to having every view function having a ‘product’ parameter in it’s header. The views should be as agnostic about the whole tenant thing, as possible.
So I wrote a simple piece of middleware, that helped me, load the correct model, the ‘product’ parameter refered to, tie that model object to the request object, so the view didn’t have to know about the product, if it didn’t need to and delete the old ‘product’ parameter from the request parameter stack.
I also needed the tenant’s metadata in the base view template, so I attached said product to the template context, after the view did process.
The sourcecode for that middle-ware looks like this:
from commons.models import Product from django.shortcuts import get_object_or_404 import inspect class ProductContext: def process_view(self, request, view_func, view_args, view_kwargs): if 'product' in view_kwargs: productname = view_kwargs['product'] del view_kwargs['product'] product = get_object_or_404(Product, name=productname) request.product = product if 'product' in inspect.getargspec(view_func): view_kwargs['product'] = product def process_template_response(self, request, response): if hasattr(request, 'product'): response.context_data['product'] = request.product; return response
Finishing Touches: The Models
Great, we are already getting somewhere. Now I could be as agnostic as I wanted about the whole tenant business in the views, but still had the tenant information in the request if I needed it, and I could refer to metadata of the tenant in the templates.
There was one last thing, that was bugging me, and that was, that if I wanted to load or define any tenant-dependend database model, I still needed to give way to much details over and over again and Copy/Pasting code really hurts in the soul.
So I needed to take a closer look at my models too:
Step one, was to define an abstract base model, that defined the tenant relationship in any model I needed, so I could extend that whenever needed:
class Product(models.Model): """The Tenant of our applications, to which all tenant related models should refer. """ name = models.CharField(max_length=64) def __unicode__(self): return self.name class TenantModel(models.Model): related_product = models.ForeignKey(Product) class Meta: abstract = True
Any model, that would need to be seen in a tenant-related context, could now simply derive from TenantModel, instead of from the basic models.Model.
With that, my models now didn’t need to know about the tenants either.
There was only one last things to do now:
At the moment, when I wanted to load a tenant-related model in the view, the call looked something like this.
def a_view(request): an_object_lst = SomeModel.objects().filter( related_product=request.product) return TemplateResponse(request, 'a_template.html', locals())
See that? All our carefully constructed separation of concerns about the tenant information now falls to pieces, because we need to know about the relation between our Model and it’s tenant information.
To solve that problem, I introduced a new function into the python module that defines the TenantModel. This function should replace the default call to SomeModel.objects()... and return a pre-filtered queryset, so that the view could be agnostic about the whole tenant-business again.
def objects(tenant_model, request): if issubclass(tenant_model, TenantModel): return tenant_model.objects.filter( related_product=request.product) else: return tenant_model.objects()
With that function in the bag, we can now rewrite our view:
def a_view(request): an_object_lst = objects(SomeModel, request).all() return TemplateResponse(request, 'a_template.html', locals())
Ah, now that looks much better.
The view doesn’t know about the relationship of any tenant and a given object. All it cares about, is that it needs to call a different objects function for any model instances it wants to load.
While it is true, that django does not do multi-tenancy out of the box, the clear middleware design of the framework does make it quite simple, to introduce that functionality.
If I find the time, I will separate this logic from my own project into a clean middleware app, for anyone to use and so forth.
Until then, feel free to use the information in this Blog, to build your own multi-tenant apps.
If this article helped you or inspired you, please leave a comment.