mysql - modelos - Aplicaciones Django multiusuario: ¿altera la conexión de la base de datos por solicitud?
django mysql (3)
Estoy buscando código de trabajo e ideas de otros que han intentado construir una aplicación Django de varios inquilinos usando aislamiento a nivel de base de datos.
Actualización / Solución: terminé de resolver esto en un nuevo proyecto de código abierto: vea django-db-multitenant
Gol
Mi objetivo es multiplexar solicitudes a medida que ingresan a un solo servidor de aplicaciones (interfaz WSGI como gunicorn), según el nombre de host de la solicitud o la ruta de solicitud (por ejemplo, foo.example.com/
establece la conexión de Django para usar la base de datos foo
, y bar.example.com/
usa la bar
base de datos).
Precedente
Conozco algunas soluciones existentes para multi-tenencia en Django:
- django-tenant-schemas : Esto es muy parecido a lo que quiero: instala su middleware con la precedencia más alta, y envía un comando
SET search_path
al db. Desafortunadamente, es específico de Postgres y estoy atascado con MySQL. - django-simple-multitenant : la estrategia aquí es agregar una clave externa "inquilino" a todos los modelos, y ajustar toda la lógica de negocios de la aplicación para eliminarla. Básicamente, cada fila se indexa por
(id, tenant_id)
lugar de(id)
. He intentado, y no me gusta, este enfoque por una serie de razones: hace que la aplicación sea más compleja, puede provocar errores difíciles de encontrar y no proporciona aislamiento a nivel de base de datos. - Un {servidor de aplicaciones, archivo de configuración django con db apropiado por arrendatario. La tenencia múltiple del hombre pobre de Aka (en realidad el hombre rico, dados los recursos que implica). No quiero activar un nuevo servidor de aplicaciones por inquilino, y para la escalabilidad quiero que cualquier servidor de aplicaciones pueda enviar solicitudes para cualquier cliente.
Ideas
Mi mejor idea hasta ahora es hacer algo como django-tenant-schemas
: en el primer middleware, agarre django.db.connection
y juegue con la selección de la base de datos en lugar del esquema. No he pensado bien qué significa esto en términos de conexiones agrupadas / persistentes
Otro callejón sin salida que busqué fueron los prefijos de mesa específicos del inquilino: dejando a un lado que necesitaría que fueran dinámicos, incluso un prefijo de tabla global no se logra fácilmente en Django (ver el ticket 5000 rechazado , entre otros).
Finalmente, el soporte de base de datos múltiple de Django le permite definir múltiples bases de datos con nombre y mux entre ellas en función del tipo de instancia y el modo de lectura / escritura. No es útil ya que no hay posibilidad de seleccionar la base de datos por solicitud.
Pregunta
¿Alguien ha logrado algo similar? Si es así, ¿cómo lo implementaste?
He hecho algo similar que es el más cercano al punto 1, pero en lugar de usar middleware para establecer una conexión predeterminada, se usan los enrutadores de base de datos Django. Esto permite que la lógica de la aplicación use varias bases de datos si es necesario para cada solicitud. Depende de la lógica de la aplicación elegir una base de datos adecuada para cada consulta, y este es el gran inconveniente de este enfoque.
Con esta configuración, todas las bases de datos se enumeran en la settings.DATABASES
. settings.DATABASES
, incluidas las bases de datos que pueden compartirse entre los clientes. Cada modelo que es específico del cliente se coloca en una aplicación de Django que tiene una etiqueta de aplicación específica.
p.ej. La siguiente clase define un modelo que existe en todas las bases de datos de clientes.
class MyModel(Model):
....
class Meta:
app_label = ''customer_records''
managed = False
Se coloca un enrutador de base de datos en la settings.DATABASE_ROUTERS
Cadena de bases de datos para enrutar la solicitud de base de datos por app_label
, algo como esto (no es un ejemplo completo):
class AppLabelRouter(object):
def get_customer_db(self, model):
# Route models belonging to ''myapp'' to the ''shared_db'' database, irrespective
# of customer.
if model._meta.app_label == ''myapp'':
return ''shared_db''
if model._meta.app_label == ''customer_records'':
customer_db = thread_local_data.current_customer_db()
if customer_db is not None:
return customer_db
raise Exception("No customer database selected")
return None
def db_for_read(self, model, **hints):
return self.get_customer_db(model, **hints)
def db_for_write(self, model, **hints):
return self.get_customer_db(model, **hints)
La parte especial de este enrutador es la llamada thread_local_data.current_customer_db()
. Antes de ejercer el enrutador, la persona que llama / la aplicación debe haber configurado el db del cliente actual en thread_local_data
. Se puede usar un administrador de contexto de Python para este propósito para impulsar / abrir una base de datos de clientes actual.
Con todo esto configurado, el código de la aplicación se parece a esto, donde UseCustomerDatabase
es un administrador de contexto para insertar / insertar un nombre de base de datos de cliente actual en thread_local_data
para que thread_local_data.current_customer_db()
devuelva el nombre de la base de datos correcta cuando el enrutador finalmente golpear:
class MyView(DetailView):
def get_object(self):
db_name = determine_customer_db_to_use(self.request)
with UseCustomerDatabase(db_name):
return MyModel.object.get(pk=1)
Esta es una configuración bastante compleja ya. Funciona, pero intentaré resumir lo que veo como ventajas y desventajas:
Ventajas
- La selección de la base de datos es flexible. Permite el uso de múltiples bases de datos en una sola consulta, tanto las bases de datos compartidas como las específicas de los clientes se pueden usar en una solicitud.
- La selección de la base de datos es explícita (no estoy seguro si esto es una ventaja o una desventaja). Si intenta ejecutar una consulta que llega a una base de datos de clientes pero la aplicación no ha seleccionado una, se producirá una excepción que indica un error de programación.
- El uso de un enrutador de base de datos permite que existan diferentes bases de datos en hosts diferentes, en lugar de confiar en un
USE db;
declaración que adivina que todas las bases de datos son accesibles a través de una sola conexión.
Desventajas
- Es complejo de instalar, y hay bastantes capas involucradas para que funcione.
- La necesidad y el uso de datos locales de subprocesos es oscuro.
- Las vistas están llenas de código de selección de base de datos. Esto podría abstraerse utilizando vistas basadas en clases para elegir automáticamente una base de datos basada en los parámetros de solicitud de la misma manera que el middleware elegiría una base de datos predeterminada.
- El administrador de contexto para elegir una base de datos debe estar envuelto alrededor de un conjunto de consultas de tal manera que el administrador de contexto aún esté activo cuando se evalúa la consulta.
Sugerencias
Si desea un acceso flexible a la base de datos, sugeriría usar los enrutadores de base de datos de Django. Use Middleware o una vista Mixin que configure automáticamente una base de datos predeterminada para usar para la conexión basada en los parámetros de solicitud. Es posible que tenga que recurrir a los datos locales de subprocesos para almacenar la base de datos predeterminada que se usará para que cuando se golpee el enrutador, sepa a qué base de datos debe dirigirse. Esto le permite a Django usar sus conexiones persistentes existentes a una base de datos (que puede residir en diferentes hosts si así lo desea), y elige la base de datos a usar en función de la configuración de enrutamiento en la solicitud.
Este enfoque también tiene la ventaja de que la base de datos para una consulta se puede anular si es necesario mediante el uso de la función QuerySet using()
para seleccionar una base de datos distinta de la predeterminada.
Para el registro, elegí implementar una variación de mi primera idea: emitir un USE <dbname>
en un middleware de solicitud temprana. También puse el prefijo CACHE de la misma manera.
Lo estoy usando en un sitio de producción pequeño, buscando el nombre del inquilino en una base de datos de Redis basada en el host de solicitud. Hasta ahora, estoy bastante contento con los resultados.
Lo he convertido en un proyecto de github (con suerte resuable) aquí: django-db-multitenant
Puede crear un middleware simple que determine el nombre de la base de datos de su subdominio o lo que sea y luego ejecute una declaración USE en el cursor de la base de datos para cada solicitud. Mirando el código del esquema django-tenants, eso es esencialmente lo que está haciendo. Está sub-clasificando psycopg2 y emitiendo el postgres equivalente a USE, "set search_path XXX". También podría crear un modelo para administrar y crear sus inquilinos, pero luego volvería a escribir gran parte del esquema django-tenants.
No debe haber ninguna penalización de rendimiento o recursos en MySQL para cambiar el esquema (nombre de la base de datos). Simplemente está configurando un parámetro de sesión para la conexión.