Django Tenant with Shared Schema and Database

I would like to use tenants with a shared schema and database approach, as native to Django as possible > so without any hacks like Thread Local of Context Variables.

I was thinking of writing a Custom Manager for each model (QuerySet or modelmanager) that enforces that upon selecting “objects” you have to define a tenant in a view. This should be enforced (an error is raised if no selection is made).

A bit like Post.objects(tenant=tenant) or Post.objects.tenant_filter(tenant=tenant).

I will than create mixins in all views to make sure this argument is added to all object queries. This is similar to the library of django-scopes, but more strict/enforced.

Copilot response (not working though)


from django.db import models

class TenantQuerySet(models.QuerySet):
    def tenant_filter(self, tenant):
        return self.filter(tenant=tenant)

class TenantManager(models.Manager):
    def get_queryset(self):
        return TenantQuerySet(self.model, using=self._db)

    def tenant_filter(self, tenant):
        return self.get_queryset().tenant_filter(tenant)

Thanks in advance.

You’ll need to share a traceback and describe specifically what’s not working in order to get help. It’s also very helpful if you include a minimal code snippet that includes the steps to reproduce the issue.

This question is more about creating a new way on how to do tenants in Django, potentially for a new extension package, since I did not find any one that fulfills the requirement. (i already have a working site with a single tenant).

Requirement
A shared schema tenant / site Django implementation that makes sure that always a tenant is filtered for a model. The implementation should be compliant with the Django design philosophy (separating Models and requests).

The inspriration comes from the package django-scopes: GitHub - raphaelm/django-scopes: Safely separate multiple tenants in a Django database

It is a neat package that uses Context variables to filter a model via the modified ModelManager:

def ScopedManager(_manager_class=models.Manager, **scopes):
    required_scopes = set(scopes.keys())

    class Manager(_manager_class):
        def __init__(self):
            super().__init__()

        def get_queryset(self):
            current_scope = get_scope()
            if not current_scope.get('_enabled', True):
                return super().get_queryset()
            missing_scopes = required_scopes - set(current_scope.keys())
            if missing_scopes:
                return DisabledQuerySet(self.model, using=self._db, missing_scopes=missing_scopes)
            else:
                filter_kwargs = {}
                for dimension in required_scopes:
                    current_value = current_scope[dimension]
                    if isinstance(current_value, (list, tuple)):
                        filter_kwargs[scopes[dimension] + '__in'] = current_value
                    elif current_value is not None:
                        filter_kwargs[scopes[dimension]] = current_value
                return super().get_queryset().filter(**filter_kwargs)

        def all(self):
            a = super().all()
            if isinstance(a, DisabledQuerySet):
                a = a.all()
            return a

    return Manager()

I think their tenant / site integration is elegant:

from django_scopes import ScopedManager

class Post(models.Model):
	site = models.ForeignKey(Site, …)
	title = models.CharField(…)

	objects = ScopedManager(site='site')

class Comment(models.Model):
	post = models.ForeignKey(Post, …)
	text = models.CharField(…)

	objects = ScopedManager(site='post__site')

By putting a WITH in the middleware, a tenant can be selected:

Sounds cumbersome to put those with statements everywhere? Maybe not at all: You probably already have a middleware that determines the site (or tenant, in general) for every request based on URL or logged in user, and you can easily use it there to just automatically wrap it around all your tenant-specific views.

I want to do something similar, but with native Django functionality. To enforce all views, that use objects, to filter the objects like:

Post.objects.with_tenant(site=site).

If the with_tenant filter is forgotten, an error should be raised.

To be able to filter all tenants or objects without a tenant, I would create two specific tenants like “public”, “no_tenant”, which the model manager would recognize and filter correctly as well.

Any ideas how to do this effectively?

want to do something similar, but with native Django functionality. To enforce all views, that use objects, to filter the objects like:

What does “native Django functionality” mean?

Any ideas how to do this effectively?

It seems like the library is achieving it effectively

From your OP:

I would like to use tenants with a shared schema and database approach, as native to Django as possible > so without any hacks like Thread Local of Context Variables.

I was thinking of writing a Custom Manager for each model (QuerySet or modelmanager) that enforces that upon selecting “objects” you have to define a tenant in a view. This should be enforced (an error is raised if no selection is made).

The library seems to be accomplishing your goals.

>>> Comment.objects.all()
ScopeError: A scope on dimension "site" needs to be active for this query.

I wouldn’t get caught up on the fact that it’s using thread locals specifically. I can’t think of another way to pass state into the objects manager on a per request basis without setting a “magic thread local” somewhere. Thread locals aren’t hacks; they come with tradeoffs, but sometimes they are the right tool for the job. This seems like an appropriate use to me.

Thanks for the quick reply. It does not, as it uses context variables.

Native I mean is that I enforce it by raising an error it it is not specified in the View. Native in the sense to keep request and models separate as per Django’s design.

As mentioned in the thread: django multi tenancy issue, thread local does not work in async. Thus if feels like a suboptimal solution and not fully compatible with Django.

It should not be that hard to enforce this at the modelmanager level ?

A possible solution would be to overwrite all “get, filter, all” methods for example and validate that the required filter (tenant / site) is set as a kwarg. I search a way that I do not need to overwrite all these methods but can overwrite a single one?

As mentioned in the thread: django multi tenancy issue , thread local does not work in async. Thus if feels like a suboptimal solution and not fully compatible with Django.

this solution does not seem sub optimal to me. if the library doesn’t work with async, and using async is a requirement, then make a pr or fork the code to use contextvars to handle the magic.

It should not be that hard to enforce this at the modelmanager level ?

It seems challenging to achieve your goal because Model.objects is instantiated once in the global namespace. Instead, you could make a class method that returns a queryset that applies your filter.

class Obj(models.Model):
    @classmethod
    def tenant_objects(cls, tenant):
        return cls.objects.filter(tenant=tenant)

Obj.tenant_objects(tenant="public").all()

Thanks. This is an a nice option, but do you see a model manager option as well?

Basically I would like to attach the objects attribute to a model manager Class that extends the normal manager class and has only one method called with_tenant(). All other methods like filter and all do not exist in this class to prevent not selecting a tenant.

But context variables are safe you say for async as well? The Django scopes already use context variables.

But context variables are safe you say for async as well? The Django scopes already use context variables.

yes

Thanks. This is an a nice option, but do you see a model manager option as well?

I do not see a reasonable one

Basically I would like to attach the objects attribute to a model manager Class that extends the normal manager class and has only one method called with_tenant(). All other methods like filter and all do not exist in this class to prevent not selecting a tenant.

This seems like a paradox.