¿Qué es y como implementarlo?
Multi-tenant es una arquitectura que permite a una instancia única de software proveer servicio a varios clientes.
Cada cliente es considerado un Tenant.
Cada tenant puede personalizar ciertas partes de la aplicación (colores de la interfaz por ejemplo) pero no puede personalizar el código.
Actúa como nombre de espacio (Postgres).
Añade una capa adicional de organización/separación a la base de datos.
(Database -> Schema -> Tabla)
*Cuando no se especifica un schema en la conexión or consulta, solo se accede al schema publico*
Una Base de datos - Un schema
Pros
Contras
Cada tenant tiene su propia base de datos
Pros
Contras
Una base de datos - Multiples schemas
Pros
Contras
Cada tenant (cliente) es identificado por su hostname por ejemplo (dbguiance.platzi.com).
La información del cliente y hostname es almacenada en una tabla en el schema public.
Cuando una petición es realizada, el hostname es usado para buscar el tenant en la base de datos.
Si no se encuentra un tenant con el hostname es lanzado un error 404
La mayoría de aplicaciones dentro de un proyecto pueden ser especificas, esto significa que la información en la base de datos no se comparte con otros tenants.
Por ejemplo si hablamos de un software de tiendas on-line podemos tener Clientes, Proveedores, Facturas, etc...
A diferencia de las anteriores, estas son aplicaciones que pueden compartir información entre todos los tenants.
Seguimos hablando de la tienda on-line puede existir por ejemplo una tabla con los porcentajes de los impuestos, o tarifas de servicios de envíos, etc...
pip install django-tenant-schemas
En el archivo 'settings.py'
DATABASES = {
'default': {
'ENGINE': 'tenant_schemas.postgresql_backend',
# ..
}
}
DATABASE_ROUTERS = (
'tenant_schemas.routers.TenantSyncRouter',
)
MIDDLEWARE_CLASSES = (
'tenant_schemas.middleware.TenantMiddleware',
# 'tenant_schemas.middleware.SuspiciousTenantMiddleware',
#...
)
Si se desea lanzar un error 400 en lugar de un 404 cuando no se encuentra un tenant valido se usa `tenant_schemas.middleware.SuspiciousTenantMiddleware`
Es necesario asegurarse que `django.core.context_processors.request` se encuentre dentro de `TEMPLATE_CONTEXT_PROCESSORS`
Es el modelo donde se guarda la relación entre el dominio (subdominio) y el nombre del schema, puede tener más campos pero tiene que heredar de `TenantMixin`
from django.db import models
from tenant_schemas.models import TenantMixin
class Client(TenantMixin):
name = models.CharField(max_length=100)
paid_until = models.DateField()
on_trial = models.BooleanField()
created_on = models.DateField(auto_now_add=True)
# default true, schema will be automatically created
# and synced when it is saved
auto_create_schema = True
Una vez creado el modelo se crea el archivo de migración.
python manage.py makemigrations <nombre modelo>
Se crean dos tuplas una `SHARED_APPS` y la otra `TENANT_APPS`
En la primera se ponen las aplicaciones que se van a estar en el schema public y visibles por todas los tenants.
En la segunda se ponen las aplicaciones que tienen información separada por tenant
SHARED_APPS = (
'tenant_schemas', # mandatory
'customers', # you must list the app where your tenant model resides in
'django.contrib.contenttypes',
# everything below here is optional
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.admin',
)
TENANT_APPS = (
# The following Django contrib apps must be in TENANT_APPS
'django.contrib.contenttypes',
# your tenant-specific apps
'myapp.hotels',
'myapp.houses',
)
INSTALLED_APPS = list(SHARED_APPS
) + [app for app in TENANT_APPS if app not in SHARED_APPS]
Se configura el modelo tenant
Ejecutar
TENANT_MODEL = 'customer.Client"
Nunca usar ```./manage.py migrate```
./manage.py migrate_schemas
Default: public
Nombre del schema que sera tratado como público
Default: True
Define si los models son migrados a directamente a la última versión y las migraciones posteriores son falseadas
Default: None
Configura las urls usadas cuando se visita el sitio principal.
Si consideramos que tenemos cliente.example.com y example.com la url de /login por ejemplo seria la misma en las dos urls. Definiendo este parámetro se consigue que las urls publicas sean diferentes a las compartidas por el tenant.
En este caso se usa las url definidas en PUBLIC_SCHEMA_URLCONF en lugar de las urls de ROOT_URLCONF
Todos los comandos excepto `tenant_commant` corren para todos los tenants.
Para correr un comando para tenants especificos es necesario heredar de `BaseTenantCommand`
# foo/management/commands/do_foo.py
from django.core.management.base import BaseCommand
class Command(BaseCommand):
def handle(self, *args, **options):
do_foo()
# foo/management/commands/tenant_do_foo.py
from tenant_schemas.management.commands import BaseTenantCommand
class Command(BaseTenantCommand):
COMMAND_NAME = 'do_foo'
Para correr el comando para un tenant en particular se usa el paramentro
--schema=<nombre schema>
Corre cualquier comando en un schema individual.
Si no se especifica schema, el prompt solicitara el nombre del schema, o se puede especificar así:
Ya tiene configurado por defecto el parametro o flag schema
./manage.py tenant_command loaddata
./manage.py tenant_command loaddata --schema=<nombre schema>
./manage.py createsuperuser --useradmin='admin' --schema=<nombre schema>
Listado de schemas: domain_url de cada tenant creado
./manage.py list_tenants
Context manager para un schema especifico
Context manager para un tenant especifico
Devuelve True si un schema ya existe en la base de datos
Devuelve el nombre de modelo tenant
Devuelve el nombre del schema publico
Soporta Celery usando tenant-schemas-celery
Es necesario agregar los routes a urls.py manualmente.
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)),
)
Django no crea los tenants durante los test, para esto existe TenantTestCase.
from tenant_schemas.test.cases import TenantTestCase
from tenant_schemas.test.client import TenantClient
class BaseSetup(TenantTestCase):
def setUp(self):
self.c = TenantClient(self.tenant)
def test_user_profile_view(self):
response = self.c.get(reverse('user_profile'))
self.assertEqual(response.status_code, 200)
Tambien existe `TenantRequestFactory` y `TenantClient` los cuales toman el dominio del tenant automagicamente.