2013-02-28 01:28:46 +01:00
===========================
Using django-tenant-schemas
===========================
2017-04-12 07:08:55 +02:00
2016-11-10 21:43:49 +01:00
Supported versions
------------------
You can use `` django-tenant-schemas `` with currently maintained versions of Django -- see the `Django's release process <https://docs.djangoproject.com/en/1.10/internals/release-process/> `_ and the present list of `Supported Versions <https://www.djangoproject.com/download/#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.2, the minimum for Django 1.10.
2013-11-15 00:50:05 +01:00
Creating a Tenant
2013-02-28 01:28:46 +01:00
-----------------
2017-04-12 07:08:55 +02:00
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 `` .
2013-11-03 09:54:30 +01:00
2013-11-03 10:03:37 +01:00
.. code-block :: python
2013-02-28 01:28:46 +01:00
2013-08-14 20:46:23 +02:00
from customers.models import Client
2013-11-15 00:50:05 +01:00
2013-02-28 01:28:46 +01:00
# create your public tenant
2013-06-19 04:47:52 +02:00
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
2013-11-15 00:50:05 +01:00
schema_name='public',
2013-02-28 01:28:46 +01:00
name='Schemas Inc.',
paid_until='2016-12-05',
on_trial=False)
tenant.save()
2013-11-15 00:50:05 +01:00
2013-11-03 09:54:30 +01:00
Now we can create our first real tenant.
2013-11-03 10:03:37 +01:00
.. code-block :: python
2013-02-28 01:28:46 +01:00
2013-08-14 20:46:23 +02:00
from customers.models import Client
2013-11-15 00:50:05 +01:00
2013-02-28 01:28:46 +01:00
# create your first real tenant
2013-06-19 04:47:52 +02:00
tenant = Client(domain_url='tenant.my-domain.com', # don't add your port or www here!
2013-11-15 00:50:05 +01:00
schema_name='tenant1',
2013-02-28 01:28:46 +01:00
name='Fonzy Tenant',
paid_until='2014-12-05',
on_trial=True)
2015-07-31 16:12:22 +02:00
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.
2013-03-02 16:30:08 +01:00
2013-11-03 09:54:30 +01:00
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.
2013-02-28 01:28:46 +01:00
Management commands
-------------------
2017-04-12 07:08:55 +02:00
By default, base commands run on the public tenant but you can also own commands that run on a specific tenant by inheriting `` BaseTenantCommand `` .
2015-04-10 02:57:58 +02:00
2015-07-31 16:02:36 +02:00
For example, if you have the following `` do_foo `` command in the `` foo `` app:
2015-04-10 02:57:58 +02:00
2015-07-31 16:02:36 +02:00
`` foo/management/commands/do_foo.py ``
2015-04-10 02:57:58 +02:00
.. code-block :: python
from django.core.management.base import BaseCommand
class Command(BaseCommand):
def handle(self, *args, * *options):
do_foo()
2017-04-12 07:08:55 +02:00
You could create a wrapper command by using `` BaseTenantCommand `` :
2015-04-10 02:57:58 +02:00
2015-07-31 16:02:36 +02:00
`` foo/management/commands/tenant_do_foo.py ``
2015-04-10 02:57:58 +02:00
.. code-block :: python
from tenant_schemas.management.commands import BaseTenantCommand
class Command(BaseTenantCommand):
COMMAND_NAME = 'do_foo'
2017-04-12 07:08:55 +02:00
To run the command on a particular schema, there is an optional argument called `` --schema `` .
2013-11-03 09:54:30 +01:00
2013-11-03 10:03:37 +01:00
.. code-block :: bash
2013-02-28 01:28:46 +01:00
2017-04-12 07:08:55 +02:00
./manage.py tenant_command do_foo --schema=customer1
If you omit the `` schema `` argument, the interactive shell will ask you to select one.
2013-02-28 01:28:46 +01:00
Add TenantContextFilter - put schema_name and domain_url in log records
Add a LOGGING configuration similar to the section below.
LOGGING = {
'filters': {
'tenant_context': {
'()': 'tenant_schemas.log.TenantContextFilter',
},
},
'formatters': {
'default': {
'format': '[%(schema_name)s:%(domain_url)s] '
'%(levelname)s %(asctime)s '
'%(message)s',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'default',
'filters': ['tenant_context'],
},
},
}
Your logging output will not look something like:
[example:example.com] DEBUG 13:29 django.db.backends: (0.001) SELECT ...
2016-08-24 09:49:13 +02:00
migrate_schemas
2015-07-31 16:12:22 +02:00
~~~~~~~~~~~~~~~
2014-12-25 21:06:11 +01:00
2016-05-17 11:34:48 +02:00
`` 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.
2013-03-02 16:30:08 +01:00
.. warning ::
2015-07-31 16:12:22 +02:00
You should never directly call `` migrate `` . We perform some magic in order to make `` migrate `` only migrate the appropriate apps.
2013-02-28 01:28:46 +01:00
2013-11-03 10:03:37 +01:00
.. code-block :: bash
2013-11-03 09:54:30 +01:00
2015-07-31 16:12:22 +02:00
./manage.py migrate_schemas
2013-11-15 00:50:05 +01:00
2015-07-31 16:12:22 +02:00
The options given to `` migrate_schemas `` are also passed to every `` migrate `` . Hence you may find handy
2013-11-03 09:54:30 +01:00
2013-11-03 10:03:37 +01:00
.. code-block :: bash
2013-02-28 01:28:46 +01:00
2015-07-31 16:12:22 +02:00
./manage.py migrate_schemas --list
2013-02-28 01:28:46 +01:00
2016-04-07 17:23:41 +02:00
`` migrate_schemas `` raises an exception when an tenant schema is missing.
2016-02-20 21:17:39 +01:00
2015-07-31 16:12:22 +02:00
tenant_command
2014-12-25 21:06:11 +01:00
~~~~~~~~~~~~~~
2013-11-03 09:54:30 +01:00
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
2013-09-09 22:55:00 +02:00
2013-11-03 10:03:37 +01:00
.. code-block :: bash
2013-09-09 22:55:00 +02:00
2014-12-25 21:06:11 +01:00
./manage.py tenant_command loaddata
2013-09-09 22:55:00 +02:00
2013-11-03 09:54:30 +01:00
If you don't specify a schema, you will be prompted to enter one. Otherwise, you may specify a schema preemptively
2013-09-09 22:55:00 +02:00
2013-11-03 10:03:37 +01:00
.. code-block :: bash
2013-09-09 22:55:00 +02:00
2014-12-25 21:06:11 +01:00
./manage.py tenant_command loaddata --schema=customer1
Add TenantContextFilter - put schema_name and domain_url in log records
Add a LOGGING configuration similar to the section below.
LOGGING = {
'filters': {
'tenant_context': {
'()': 'tenant_schemas.log.TenantContextFilter',
},
},
'formatters': {
'default': {
'format': '[%(schema_name)s:%(domain_url)s] '
'%(levelname)s %(asctime)s '
'%(message)s',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'default',
'filters': ['tenant_context'],
},
},
}
Your logging output will not look something like:
[example:example.com] DEBUG 13:29 django.db.backends: (0.001) SELECT ...
2016-08-24 09:49:13 +02:00
createsuperuser
2014-12-25 21:06:11 +01:00
~~~~~~~~~~~~~~~
The command `` createsuperuser `` is already automatically wrapped to have a `` schema `` flag. Create a new super user with
.. code-block :: bash
2016-10-21 13:26:02 +02:00
./manage.py createsuperuser --username=admin --schema=customer1
2014-08-06 03:28:46 +02:00
2015-07-29 01:44:32 +02:00
list_tenants
~~~~~~~~~~~~
Prints to standard output a tab separated list of schema:domain_url values for each tenant.
.. code-block :: bash
for t in $(./manage.py list_tenants | cut -f1);
do
2015-08-01 15:14:36 +02:00
./manage.py tenant_command dumpdata --schema=$t --indent=2 auth.user > ${t}_users.json;
2015-07-29 01:44:32 +02:00
done
2017-02-22 10:08:37 +01:00
Storage
-------
The :mod: `~django.core.files.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.
.. code-block :: python
# settings.py
MEDIA_ROOT = '/data/media'
MEDIA_URL = '/media/'
DEFAULT_FILE_STORAGE = 'tenant_schemas.storage.TenantFileSystemStorage'
We provide :class: `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.
.. code-block :: text
# 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/;
}
}
2016-06-22 16:15:51 +02:00
Utils
-----
There are several utils available in `tenant_schemas.utils` that can help you in writing more complicated applications.
2016-10-03 22:44:00 +02:00
.. function :: schema_context(schema_name)
2016-06-22 16:15:51 +02:00
2016-10-03 22:44:00 +02:00
This is a context manager. Database queries performed inside it will be executed in against the passed `` schema_name `` .
2016-06-22 16:15:51 +02:00
.. code-block :: python
from tenant_schemas.utils import schema_context
with schema_context(schema_name):
2016-10-03 22:44:00 +02:00
# All comands here are ran under the schema `schema_name`
2016-06-22 16:15:51 +02:00
2016-10-03 22:44:00 +02:00
# Restores the `SEARCH_PATH` to its original value
2016-06-22 16:15:51 +02:00
2016-10-03 22:44:00 +02:00
.. function :: tenant_context(tenant_object)
2016-06-22 16:15:51 +02:00
2016-10-03 22:44:00 +02:00
This context manager is very similiar to the `` schema_context `` function,
but it takes a tenant model object as the argument instead.
2016-06-22 16:15:51 +02:00
.. code-block :: python
from tenant_schemas.utils import tenant_context
with tenant_context(tenant):
2016-10-03 22:44:00 +02:00
# All commands here are ran under the schema from the `tenant` object
2016-06-22 16:15:51 +02:00
2016-10-03 22:44:00 +02:00
# Restores the `SEARCH_PATH` to its original value
2016-06-22 16:15:51 +02:00
2016-10-03 22:44:00 +02:00
.. function :: schema_exists(schema_name)
2016-06-22 16:15:51 +02:00
2016-10-03 22:44:00 +02:00
Returns `` True `` if a schema exists in the current database.
2016-06-22 16:15:51 +02:00
.. code-block :: python
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
2016-10-03 22:44:00 +02:00
.. function :: get_tenant_model()
2016-06-22 16:15:51 +02:00
Returns the class of the tenant model.
2016-10-03 22:44:00 +02:00
.. function :: get_public_schema_name()
2016-06-22 16:15:51 +02:00
2016-10-03 22:44:00 +02:00
Returns the name of the public schema (from settings or the default `` public `` ).
2016-06-22 16:15:51 +02:00
2016-10-03 22:44:00 +02:00
.. function :: get_limit_set_calls()
2016-06-22 16:15:51 +02:00
2016-10-03 22:44:00 +02:00
Returns the `` TENANT_LIMIT_SET_CALLS `` setting or the default (`` False `` ). See below.
2016-06-22 16:15:51 +02:00
Add TenantContextFilter - put schema_name and domain_url in log records
Add a LOGGING configuration similar to the section below.
LOGGING = {
'filters': {
'tenant_context': {
'()': 'tenant_schemas.log.TenantContextFilter',
},
},
'formatters': {
'default': {
'format': '[%(schema_name)s:%(domain_url)s] '
'%(levelname)s %(asctime)s '
'%(message)s',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'default',
'filters': ['tenant_context'],
},
},
}
Your logging output will not look something like:
[example:example.com] DEBUG 13:29 django.db.backends: (0.001) SELECT ...
2016-08-24 09:49:13 +02:00
Logging
-------
The optional `` TenantContextFilter `` can be included in `` settings.LOGGING `` to add the current `` schema_name `` and `` domain_url `` to the logging context.
.. code-block :: python
# settings.py
2017-02-22 10:08:37 +01:00
Add TenantContextFilter - put schema_name and domain_url in log records
Add a LOGGING configuration similar to the section below.
LOGGING = {
'filters': {
'tenant_context': {
'()': 'tenant_schemas.log.TenantContextFilter',
},
},
'formatters': {
'default': {
'format': '[%(schema_name)s:%(domain_url)s] '
'%(levelname)s %(asctime)s '
'%(message)s',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'default',
'filters': ['tenant_context'],
},
},
}
Your logging output will not look something like:
[example:example.com] DEBUG 13:29 django.db.backends: (0.001) SELECT ...
2016-08-24 09:49:13 +02:00
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:
.. code-block :: text
[example:example.com] DEBUG 13:29 django.db.backends: (0.001) SELECT ...
2014-08-06 03:28:46 +02:00
Performance Considerations
--------------------------
2014-12-27 16:26:13 +01:00
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:
2014-08-06 03:28:46 +02:00
.. code-block :: python
2017-02-22 10:08:37 +01:00
# settings.py:
2014-08-06 03:28:46 +02:00
TENANT_LIMIT_SET_CALLS = True
When set, `` django-tenant-schemas `` will set the search path only once per request. The default is `` False `` .
2014-08-24 06:16:52 +02:00
Third Party Apps
----------------
2015-07-31 15:59:19 +02:00
Celery
~~~~~~
2014-08-24 06:16:52 +02:00
Support for Celery is available at `tenant-schemas-celery <https://github.com/maciej-gol/tenant-schemas-celery> `_ .
2015-07-31 15:59:19 +02:00
django-debug-toolbar
~~~~~~~~~~~~~~~~~~~~
2015-07-31 16:02:36 +02:00
`django-debug-toolbar <https://github.com/django-debug-toolbar/django-debug-toolbar> `_ routes need to be added to `` urls.py `` (both public and tenant) manually.
2015-07-31 15:59:19 +02:00
.. code-block :: python
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)),
)