Package: django-db-multitenant
Provides a simple multi-tenancy solution for Django based on the concept
of having asingle tenant per database.This application is still experimental, but is being used in production
by the authors. Contributions and discussion are welcome.Read the ChangelogBackgroundMulti-tenancy is the ability to support multiple distinct datasets from
the same application server. Each dataset usually maps to a customer
(the tenant) and is partially or fully partitioned from all other tenant
data.Among the possible approaches are:Isolated approach: Separate database per tenant.Semi-isolated approach: Shared database, separate namespaces
(PostgreSQL schemas) or table names/prefix per tenant.Shared approach: Single database for all tenants. Each table has
a column identifying the tenant for that row of data.This application supports two backends, MySQL and PostgreSQL:With MySQL, this application implements a variation of theisolated approach,
each tenant has itsown database, however theirconnection details are
shared(such as password, database user).For PostgreSQL, this application implements asemi-isolated approach,
each tenant has its own schema and the connection details are shared via the
public schema.django-db-multitenant makes it possible (even easy) to take a Django
application designed for a single tenant and use it with multiple
tenants.OperationThe main technique is as follows:When a request first arrives, determine desired the tenant from therequestobject, and save it in thread-local storage.Later in the request, when a database cursor is accquired, issue an
SQLUSE <tenant database name>for the desired tenant with MySQL
orSET search_patch TO <tenant name>with PostgreSQL.Step 1 is accomplished by implementing amapper
class.
Your mapper takes a request object and returns a database name or tenant
name, using whatever logic you like (translate hostname, inspect a HTTP
header, etc). The mapper result is saved in thread-local storage for
later use.Step 2 determines whether the desired database or schema has already
been selected, and is skipped if so. This is implemented using a thin
database backend
wrapperfor MySQLandfor PostgreSQLwhich must be set insettings.DATABASESas the backend.Usage1. InstallInstalldjango-db-multitenant(or add it to your setup.py).$ pip install django-db-multitenant2. Implement a mapperYou must implement a subclass ofdb_multitenant.mapperwhich determines the database name and cache prefix from the request.To help you to write your mapper, the repository contains examples of mappers which extracts the hostname
of URL to determine the tenant name (eg. inhttps://foo.example.com/bar/,foowill be the tenant name):mapper for MySQL,
which uses a portion of the hostname directly as the database name.mapper for PostgreSQL,
which uses a portion of the hostname as search path (schema). PostgreSQL
allows complex setups with sharing of common tables (public accounts for example),
see the comment in the mapper for more details.mapper for Redis,
which looks up the tenant using the hostname, throwing a 404 if unrecognized.Feel free to copy an example mapper in your project then adjust it to your needs.3. Update settings.pySet the multitenant mapper by specifying the full dotted path to your
implementation (in this example,mapperis the name of filemapper.py):MULTITENANT_MAPPER_CLASS='myapp.mapper.TenantMapper'Install the multitenant middleware as thefirstmiddleware of the list (prior to Django
1.10, you must use theMIDDLEWARE_CLASSESsetting):MIDDLEWARE=['db_multitenant.middleware.MultiTenantMiddleware',....]Change your database backend to the multitenant wrapper:DATABASES={'default':{'ENGINE':'db_multitenant.db.backends.mysql','NAME':'devnull',}}Note: theNAMEis useless for MySQL but due to a current
limitation, the named database must exist. It may be empty and
read-only.Or for PostgreSQL:DATABASES={'default':{'ENGINE':'db_multitenant.db.backends.postgresql','NAME':'mydb',}}Optionally, add the multitenant helperKEY_FUNCTIONto your cache
definition, which will cause cache keys to be prefixed with the value ofmapper.get_cache_prefix:CACHES={'default':{'LOCATION':'127.0.0.1:11211','BACKEND':'django.core.cache.backends.memcached.MemcachedCache','KEY_FUNCTION':'db_multitenant.cache.helper.multitenant_key_func'}}4. TestsIf the tenant name of your application is extracted from the URL (as in the provided examples ofmappers), you can add
a host to your/etc/hostssuch asfoo.example.comto redirect to your localhost server.You should addfoo.example.comtoALLOWED_HOSTSlist in your Django settings and just try
to reach your application from your browser withhttp://foo.example.com:8000.The examples of mappers provide information about the way to create a tenant zone.Management CommandsIn order to use management commands (likemigrate) with the correct tenant,
inject this little hack at the end of yoursettings.py:fromdb_multitenant.utilsimportupdate_from_envupdate_from_env(database_settings=DATABASES['default'],cache_settings=CACHES['default'])If you didn’t setCACHESin your settings and you don’t intend to use a cache system,
you don’t have to pass thecache_settingsargument to the function.You can then exportTENANT_DATABASE_NAMEfor MySQL orTENANT_NAMEfor PostgreSQL
andTENANT_CACHE_PREFIXon the command line, for example:$TENANT_DATABASE_NAME=example.com./manage.pymigrateDon’t forget to create the database (MySQL) or the required schema first (PostgreSQL).That’s it. Because django-db-multitenant does not define any models,
there’s no need to add it toINSTALLED_APPS.Advantages and LimitationsThere is no one-size-fits-all solution for a data modeling problem such
as multi-tenancy (see ‘Alternatives’).AdvantagesCompatibility: Your Django application doesn’t need any awareness of
multi-tenancy. Database-level tools (such asmysqldumporpgdump)
just work.Isolation: One tenant, one database means there’s no intermingling of
tenant data (excepted if you share tables with PostgreSQL).Simplicity: Your application schemas don’t need to be cluttered with
‘Tenant’ foreign key relationships.Should work well with Django 1.6 connection persistence and
connection pooling.LimitationsUnorthodox. Django does not expect this kind of dynamic database
connection tinkering, and there could be unexpected bugs.Limited isolation. Since the same DB credentials are used for all
tenants, bugs in the mapper (or anywhere else in the app) could cause
data corruption.A valid database still needs to be specified insettings.DATABASEfor use when the connection is first established with MySQL (this should be
fixed eventually).Overhead: requests may add up to one extra query (theUSE <db_name>statement for MySQL or theSET search_path TO <tenant_name>for PostgreSQL).Alternatives and Further Readingdjango-tenant-schemasimplements a semi-isolated approach using PostgreSQL schemas (and
inspired this project, as well as the ‘Overview’ section above).Credits and LicenseCopyright 2013 mike wakerly (opensource@hoho.com)Licensed under the Apache License, Version 2.0 (the “License”); you may
not use this file except in compliance with the License. You may obtain
a copy of the License athttp://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.