Using django-tenant-schemas¶
Supported versions¶
You can use django-tenant-schemas
with currently maintained versions of Django – see the Django’s release process and the present list of Supported Versions.
It is necessary to use a PostgreSQL database. django-tenant-schemas
will ensure compatibility with the minimum required version of the latest Django release. At this time that is PostgreSQL 9.3, the minimum for Django 1.11.
Creating a Tenant¶
Creating a tenant works just like any other model in Django. The first thing we should do is to create the public
tenant to make our main website available. We’ll use the previous model we defined for Client
.
from customers.models import Client
# create your public tenant
tenant = Client(domain_url='my-domain.com', # don't add your port or www here! on a local server you'll want to use localhost here
schema_name='public',
name='Schemas Inc.',
paid_until='2016-12-05',
on_trial=False)
tenant.save()
Now we can create our first real tenant.
from customers.models import Client
# create your first real tenant
tenant = Client(domain_url='tenant.my-domain.com', # don't add your port or www here!
schema_name='tenant1',
name='Fonzy Tenant',
paid_until='2014-12-05',
on_trial=True)
tenant.save() # migrate_schemas automatically called, your tenant is ready to be used!
Because you have the tenant middleware installed, any request made to tenant.my-domain.com
will now automatically set your PostgreSQL’s search_path
to tenant1, public
, making shared apps available too. The tenant will be made available at request.tenant
. By the way, the current schema is also available at connection.schema_name
, which is useful, for example, if you want to hook to any of django’s signals.
Any call to the methods filter
, get
, save
, delete
or any other function involving a database connection will now be done at the tenant’s schema, so you shouldn’t need to change anything at your views.
Management commands¶
By default, base commands run on the public tenant but you can also own commands that run on a specific tenant by inheriting BaseTenantCommand
.
For example, if you have the following do_foo
command in the foo
app:
foo/management/commands/do_foo.py
from django.core.management.base import BaseCommand
class Command(BaseCommand):
def handle(self, *args, **options):
do_foo()
You could create a wrapper command by using BaseTenantCommand
:
foo/management/commands/tenant_do_foo.py
from tenant_schemas.management.commands import BaseTenantCommand
class Command(BaseTenantCommand):
COMMAND_NAME = 'do_foo'
To run the command on a particular schema, there is an optional argument called --schema
.
./manage.py tenant_command do_foo --schema=customer1
If you omit the schema
argument, the interactive shell will ask you to select one.
migrate_schemas¶
migrate_schemas
is the most important command on this app. The way it works is that it calls Django’s migrate
in two different ways. First, it calls migrate
for the public
schema, only syncing the shared apps. Then it runs migrate
for every tenant in the database, this time only syncing the tenant apps.
Warning
You should never directly call migrate
. We perform some magic in order to make migrate
only migrate the appropriate apps.
./manage.py migrate_schemas
The options given to migrate_schemas
are also passed to every migrate
. Hence you may find handy
./manage.py migrate_schemas --list
migrate_schemas
raises an exception when an tenant schema is missing.
migrate_schemas in parallel¶
Once the number of tenants grow, migrating all the tenants can become a bottleneck. To speed up this process, you can run tenant migrations in parallel like this:
python manage.py migrate_schemas --executor=parallel
In fact, you can write your own executor which will run tenant migrations in
any way you want, just take a look at tenant_schemas/migration_executors
.
The parallel
executor accepts the following settings:
TENANT_PARALLEL_MIGRATION_MAX_PROCESSES
(default: 2) - maximum number of processes for migration pool (this is to avoid exhausting the database connection pool)TENANT_PARALLEL_MIGRATION_CHUNKS
(default: 2) - number of migrations to be sent at once to every worker
tenant_command¶
To run any command on an individual schema, you can use the special tenant_command
, which creates a wrapper around your command so that it only runs on the schema you specify. For example
./manage.py tenant_command loaddata
If you don’t specify a schema, you will be prompted to enter one. Otherwise, you may specify a schema preemptively
./manage.py tenant_command loaddata --schema=customer1
createsuperuser¶
The command createsuperuser
is already automatically wrapped to have a schema
flag. Create a new super user with
./manage.py createsuperuser --username=admin --schema=customer1
list_tenants¶
Prints to standard output a tab separated list of schema:domain_url values for each tenant.
for t in $(./manage.py list_tenants | cut -f1);
do
./manage.py tenant_command dumpdata --schema=$t --indent=2 auth.user > ${t}_users.json;
done
Storage¶
The storage
API will not isolate media per tenant. Your MEDIA_ROOT
will be a shared space between all tenants.
To avoid this you should configure a tenant aware storage backend - you will be warned if this is not the case.
# settings.py
MEDIA_ROOT = '/data/media'
MEDIA_URL = '/media/'
DEFAULT_FILE_STORAGE = 'tenant_schemas.storage.TenantFileSystemStorage'
We provide tenant_schemas.storage.TenantStorageMixin
which can be added to any third-party storage backend.
In your reverse proxy configuration you will need to capture use a regular expression to identify the domain_url
to serve content from the appropriate directory.
# illustrative /etc/nginx/cond.d/tenant.conf
upstream web {
server localhost:8080 fail_timeout=5s;
}
server {
listen 80;
server_name ~^(www\.)?(.+)$;
location / {
proxy_pass http://web;
proxy_redirect off;
proxy_set_header Host $host;
}
location /media/ {
alias /data/media/$2/;
}
}
Utils¶
There are several utils available in tenant_schemas.utils that can help you in writing more complicated applications.
-
schema_context
(schema_name)¶
This is a context manager. Database queries performed inside it will be executed in against the passed schema_name
.
from tenant_schemas.utils import schema_context
with schema_context(schema_name):
# All comands here are ran under the schema `schema_name`
# Restores the `SEARCH_PATH` to its original value
-
tenant_context
(tenant_object)¶
This context manager is very similiar to the schema_context
function,
but it takes a tenant model object as the argument instead.
from tenant_schemas.utils import tenant_context
with tenant_context(tenant):
# All commands here are ran under the schema from the `tenant` object
# Restores the `SEARCH_PATH` to its original value
-
schema_exists
(schema_name)¶
Returns True
if a schema exists in the current database.
from django.core.exceptions import ValidationError
from django.utils.text import slugify
from tenant_schemas.utils import schema_exists
class TenantModelForm:
# ...
def clean_schema_name(self)
schema_name = self.cleaned_data["schema_name"]
schema_name = slugify(schema_name).replace("-", "")
if schema_exists(schema_name):
raise ValidationError("A schema with this name already exists in the database")
else:
return schema_name
-
get_tenant_model
()¶
Returns the class of the tenant model.
-
get_public_schema_name
()¶
Returns the name of the public schema (from settings or the default public
).
-
get_limit_set_calls
()¶
Returns the TENANT_LIMIT_SET_CALLS
setting or the default (False
). See below.
Signals¶
If you want to perform operations after creating a tenant and it’s schema is saved and synced, you won’t be able to use the built-in post_save
signal, as it sends the signal immediately after the model is saved.
For this purpose, we have provided a post_schema_sync
signal, which is available in tenant_schemas.signals
from tenant_schemas.signals import post_schema_sync
from tenant_schemas.models import TenantMixin
def foo_bar(sender, tenant, **kwargs):
...
#This function will run after the tenant is saved, its schema created and synced.
...
post_schema_sync.connect(foo_bar, sender=TenantMixin)
Logging¶
The optional TenantContextFilter
can be included in settings.LOGGING
to add the current schema_name
and domain_url
to the logging context.
# settings.py
LOGGING = {
'filters': {
'tenant_context': {
'()': 'tenant_schemas.log.TenantContextFilter'
},
},
'formatters': {
'tenant_context': {
'format': '[%(schema_name)s:%(domain_url)s] '
'%(levelname)-7s %(asctime)s %(message)s',
},
},
'handlers': {
'console': {
'filters': ['tenant_context'],
},
},
}
This will result in logging output that looks similar to:
[example:example.com] DEBUG 13:29 django.db.backends: (0.001) SELECT ...
Performance Considerations¶
The hook for ensuring the search_path
is set properly happens inside the DatabaseWrapper
method _cursor()
, which sets the path on every database operation. However, in a high volume environment, this can take considerable time. A flag, TENANT_LIMIT_SET_CALLS
, is available to keep the number of calls to a minimum. The flag may be set in settings.py
as follows:
# settings.py:
TENANT_LIMIT_SET_CALLS = True
When set, django-tenant-schemas
will set the search path only once per request. The default is False
.
Third Party Apps¶
Celery¶
Support for Celery is available at tenant-schemas-celery.
django-debug-toolbar¶
django-debug-toolbar routes need to be added to urls.py
(both public and tenant) manually.
from django.conf import settings
from django.conf.urls import include
if settings.DEBUG:
import debug_toolbar
urlpatterns += patterns(
'',
url(r'^__debug__/', include(debug_toolbar.urls)),
)