Resolved merge conflicts on docs regarding tenant and schema_name

This commit is contained in:
Bernardo Pires 2013-11-15 08:56:06 +01:00
commit 36b22bc649
4 changed files with 52 additions and 101 deletions

View File

@ -1,37 +1,37 @@
===========================
Using django-tenant-schemas
===========================
Creating a Tenant
Creating a Tenant
-----------------
This 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``.
.. code-block:: python
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',
schema_name='public',
name='Schemas Inc.',
paid_until='2016-12-05',
on_trial=False)
tenant.save()
Now we can create our first real tenant.
.. code-block:: python
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',
schema_name='tenant1',
name='Fonzy Tenant',
paid_until='2014-12-05',
on_trial=True)
tenant.save() # sync_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`` and ``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.get_schema()``, which is useful, for example, if you want to hook to any of django's signals.
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`` and ``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.
@ -43,7 +43,7 @@ Every command except tenant_command runs by default on all tenants. You can also
./manage.py sync_schemas --schema=customer1
The command ``sync_schemas`` is the most important command on this app. The way it works is that it calls Django's ``syncdb`` in two different ways. First, it calls ``syncdb`` for the ``public`` schema, only syncing the shared apps. Then it runs ``syncdb`` for every tenant in the database, this time only syncing the tenant apps.
The command ``sync_schemas`` is the most important command on this app. The way it works is that it calls Django's ``syncdb`` in two different ways. First, it calls ``syncdb`` for the ``public`` schema, only syncing the shared apps. Then it runs ``syncdb`` for every tenant in the database, this time only syncing the tenant apps.
.. warning::
@ -54,7 +54,7 @@ The options given to ``sync_schemas`` are passed to every ``syncdb``. So if you
.. code-block:: bash
./manage.py sync_schemas --migrate
You can also use the option ``--tenant`` to only sync tenant apps or ``--shared`` to only sync shared apps.
.. code-block:: bash

View File

@ -1,4 +1,3 @@
import warnings
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import connection
@ -24,8 +23,7 @@ class TenantMiddleware(object):
connection.set_schema_to_public()
hostname_without_port = remove_www_and_dev(request.get_host().split(':')[0])
TenantModel = get_tenant_model()
request.tenant = get_object_or_404(TenantModel, domain_url=hostname_without_port)
request.tenant = get_object_or_404(get_tenant_model(), domain_url=hostname_without_port)
connection.set_tenant(request.tenant)
# content type can no longer be cached as public and tenant schemas have different

View File

@ -26,12 +26,12 @@ class TenantMixin(models.Model):
def save(self, verbosity=1, *args, **kwargs):
is_new = self.pk is None
if is_new and connection.get_schema() != get_public_schema_name():
if is_new and connection.schema_name != get_public_schema_name():
raise Exception("Can't create tenant outside the public schema. Current schema is %s."
% connection.get_schema())
elif not is_new and connection.get_schema() not in (self.schema_name, get_public_schema_name()):
% connection.schema_name)
elif not is_new and connection.schema_name not in (self.schema_name, get_public_schema_name()):
raise Exception("Can't update tenant outside it's own schema or the public schema. Current schema is %s."
% connection.get_schema())
% connection.schema_name)
super(TenantMixin, self).save(*args, **kwargs)
@ -44,9 +44,9 @@ class TenantMixin(models.Model):
Drops the schema related to the tenant instance. Just drop the schema if the parent
class model has the attribute auto_drop_schema set to True.
"""
if connection.get_schema() not in (self.schema_name, get_public_schema_name()):
if connection.schema_name not in (self.schema_name, get_public_schema_name()):
raise Exception("Can't delete tenant outside it's own schema or the public schema. Current schema is %s."
% connection.get_schema())
% connection.schema_name)
if schema_exists(self.schema_name) and self.auto_drop_schema:
cursor = connection.cursor()

View File

@ -1,9 +1,7 @@
import re
from django.conf import settings
from threading import local
from django.utils.importlib import import_module
from django.core.exceptions import ImproperlyConfigured
from django.db import utils
from tenant_schemas.utils import get_public_schema_name
ORIGINAL_BACKEND = getattr(settings, 'ORIGINAL_BACKEND', 'django.db.backends.postgresql_psycopg2')
@ -19,7 +17,7 @@ def _check_identifier(identifier):
raise RuntimeError("Invalid string used for the schema name.")
class PGThread(local):
class DatabaseWrapper(original_backend.DatabaseWrapper):
"""
Indicates if the public schema should be included on the search path.
When syncing the db for creating the tables, it's useful to exclude
@ -30,20 +28,48 @@ class PGThread(local):
"""
include_public_schema = True
def __init__(self):
def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs)
self.set_schema_to_public()
def set_search_path(self, cursor):
def set_tenant(self, tenant, include_public=True):
"""
Actual search_path modification for the cursor. Database will
search schemata from left to right when looking for the object
(table, index, sequence, etc.).
Main API method to current database schema,
but it does not actually modify the db connection.
"""
self.tenant = tenant
self.schema_name = tenant.schema_name
self.include_public_schema = include_public
if self.schema_name is None:
def set_schema(self, schema_name, include_public=True):
"""
Main API method to current database schema,
but it does not actually modify the db connection.
"""
self.tenant = None
self.schema_name = schema_name
self.include_public_schema = include_public
def set_schema_to_public(self):
"""
Instructs to stay in the common 'public' schema.
"""
self.tenant = None
self.schema_name = get_public_schema_name()
def _cursor(self):
"""
Here it happens. We hope every Django db operation using PostgreSQL
must go through this to get the cursor handle. We change the path.
"""
cursor = super(DatabaseWrapper, self)._cursor()
# Actual search_path modification for the cursor. Database will
# search schemata from left to right when looking for the object
# (table, index, sequence, etc.).
if not self.schema_name:
raise ImproperlyConfigured("Database schema not set. Did your forget "
"to call set_schema() or set_tenant()?")
_check_identifier(self.schema_name)
public_schema_name = get_public_schema_name()
if self.schema_name == public_schema_name:
@ -55,78 +81,5 @@ class PGThread(local):
return cursor
def get_schema(self):
return self.schema_name
def get_tenant(self):
return self.tenant
def set_schema(self, schema_name, include_public=True):
"""
Main API method to current database schema,
but it does not actually modify the db connection.
"""
self.tenant = None
self.schema_name = schema_name
self.include_public_schema = include_public
def set_tenant(self, tenant, include_public=True):
"""
Main API method to current database schema,
but it does not actually modify the db connection.
"""
self.tenant = tenant
self.schema_name = tenant.schema_name
self.include_public_schema = include_public
if self.tenant is not None:
if self.schema_name != self.tenant.schema_name:
raise ImproperlyConfigured("Passed schema '%s' does not match tenant's schema '%s'."
% (self.schema_name, self.tenant.schema_name))
def set_schema_to_public(self):
"""
Instructs to stay in the common 'public' schema.
"""
self.tenant = None
self.schema_name = get_public_schema_name()
class DatabaseWrapper(original_backend.DatabaseWrapper):
def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs)
self.pg_thread = PGThread()
def set_tenant(self, tenant, include_public = True):
self.set_settings_schema(tenant.schema_name)
self.pg_thread.set_tenant(tenant, include_public)
def set_schema(self, schema_name, include_public = True):
self.set_settings_schema(schema_name)
self.pg_thread.set_schema(schema_name, include_public)
def set_schema_to_public(self):
self.set_settings_schema(get_public_schema_name())
self.pg_thread.set_schema_to_public()
def set_settings_schema(self, schema_name):
self.settings_dict['SCHEMA'] = schema_name
def get_schema(self):
return self.pg_thread.get_schema()
def get_tenant(self):
return self.pg_thread.get_tenant()
def _cursor(self):
"""
Here it happens. We hope every Django db operation using PostgreSQL
must go through this to get the cursor handle. We change the path.
"""
cursor = super(DatabaseWrapper, self)._cursor()
cursor = self.pg_thread.set_search_path(cursor)
return cursor
DatabaseError = original_backend.DatabaseError
IntegrityError = original_backend.IntegrityError