Configuración y Operación de Múltiples Bases de Datos en Django

Gestión de Bases de Datos Múltiples y Separación Lectura/Escritura

La impelmentación de una arquitectura de bases de datos distribuidas es una práctica común para escalar aplicaciones y mejorar la disponibilidad. Una estrategia clave en este contexto es la separación de operaciones de lectura y escritura (Read/Write Splitting). Bajo este esquema, las escrituras se dirigen a una base de datos principal (maestra), mientras que las lecturas se distribuyen entre una o más réplicas (esclavas).

Consideremos un ejemplo típico de configuración:

  • db_primaria (Maestra): Responsable de todas las transacciones de escritura, accesible en 192.168.1.2.
  • db_secundaria (Réplica): Dedicada exclusivamente a las consultas de lectura, ubicada en 192.168.1.3.

Django simplifica la administración de estas configuraciones avanzadas a través de la definición de múltiples conexiones a bases de datos y la utilización de enrutadores de bases de datos personalizados.

Aplicación de Migraciones en Entornos Multibase de Datos

Para inicializar o actualizar el esquema de la base de datos en una configuración con múltiples conexiones, el sistema de migraciones de Django permite especificar la base de datos de destino para cada operación.

El proceso comienza generando los archivos de migración estándar:

python manage.py makemigrations

Posteriormente, aplicamos estas migraciones a cada base de datos específica utilizando el alias que le hayamos asignado en la configuración de settings.py:

python manage.py migrate --database=db_primaria
python manage.py migrate --database=db_secundaria

En escanarios donde diferentes aplicaciones dentro del proyecto Django deben interactuar con bases de datos distintas, es posible dirigir las migraciones por aplicación. Por ejemplo, si app_pedidos utiliza db_primaria y app_auditoria se conecta a db_secundaria, el proceso sería:

python manage.py makemigrations
python manage.py migrate app_pedidos --database=db_primaria
python manage.py migrate app_auditoria --database=db_secundaria

Selección Explícita de Base de Datos en Consultas ORM

Django proporciona la flexibilidad para especificar directamente qué base de datos debe emplearse en una consulta ORM particular. Esta funcionalidad es invaluable para operaciones específicas, tareas administrativas o durante el proceso de depuración.

El método .using(), disponible en cualquier QuerySet, permite seleccionar la base de datos mediante su alias configurado:

# Para crear un nuevo registro en la base de datos principal
from mi_app import models

models.Cliente.objects.using("db_primaria").create(
    nombre="Juan Pérez", 
    email="juan.perez@ejemplo.com"
)

# Para recuperar todos los registros de una réplica
lista_clientes = models.Cliente.objects.using("db_secundaria").all()
for cliente in lista_clientes:
    print(f"ID: {cliente.id}, Nombre: {cliente.nombre}")

Configuración Global de Bases de Datos y Enrutadores Personalizados

Para automatizar el enrutamiento de operaciones de lectura y escritura entre distintas bases de datos, es fundamental configurar el archivo settings.py. Esto implica definir las conexiones a las bases de datos y especificar un enrutador personalizado.

Dentro de settings.py, se añaden las siguientes configuraciones:

# settings.py

# Mapeo de aplicaciones a bases de datos:
# Define qué aplicación usará qué base de datos por defecto para el enrutamiento.
APP_DB_MAPPING = {
    'app_pedidos': 'db_primaria',
    'app_auditoria': 'db_secundaria',
    'auth': 'db_primaria', # Django apps
    'admin': 'db_primaria', # Django apps
    'sessions': 'db_primaria', # Django apps
    'contenttypes': 'db_primaria', # Django apps
}

# Enrutadores de base de datos personalizados:
# La lista debe contener la ruta completa a las clases de enrutadores.
DATABASE_ROUTERS = ['mi_proyecto.routers.AppDBRouter']

# Definición de las bases de datos:
DATABASES = {
    'default': { # Siempre debe haber una base de datos 'default'
        'ENGINE': 'mssql',
        'NAME': 'db_principal_default',
        'HOST': '192.168.1.2',
        'PORT': '1433',
        'USER': 'user_primary',
        'PASSWORD': 'pass_primary',
        'OPTIONS': {
            'driver': 'ODBC Driver 17 for SQL Server',
            'MARS_Connection': True,
        },
    },
    'db_primaria': {
        'ENGINE': 'mssql',
        'NAME': 'db_pedidos_name',
        'HOST': '192.168.1.2',
        'PORT': '1433',
        'USER': 'user_primary',
        'PASSWORD': 'pass_primary',
        'OPTIONS': {
            'driver': 'ODBC Driver 17 for SQL Server',
            'MARS_Connection': True,
        },
    },
    'db_secundaria': {
        'ENGINE': 'mssql',
        'NAME': 'db_auditoria_name',
        'HOST': '192.168.1.3',
        'PORT': '1433',
        'USER': 'user_replica',
        'PASSWORD': 'pass_replica',
        'OPTIONS': {
            'driver': 'ODBC Driver 17 for SQL Server',
        },
    },
    # ... otras bases de datos si es necesario
}

El enrutador es una clase Python que contiene la lógica para determinar qué base de datos debe utilizarse para cada operación del ORM. Creamos un archivo routers.py (o similar) dentro de nuestro directorio principal de proyecto (ej., mi_proyecto/routers.py) y definimos la clase del enrutador:

# mi_proyecto/routers.py
from django.conf import settings

class AppDBRouter:
    """
    Un enrutador de base de datos que dirige las operaciones
    basándose en la aplicación a la que pertenece el modelo.
    """

    def db_for_read(self, model, **hints):
        """
        Intenta leer modelos de la base de datos asignada a su aplicación.
        """
        app_label = model._meta.app_label
        return settings.APP_DB_MAPPING.get(app_label, None)

    def db_for_write(self, model, **hints):
        """
        Intenta escribir modelos en la base de datos asignada a su aplicación.
        """
        app_label = model._meta.app_label
        return settings.APP_DB_MAPPING.get(app_label, None)

    def allow_relation(self, obj1, obj2, **hints):
        """
        Permite relaciones si ambos objetos están en la misma base de datos.
        """
        db_obj1 = settings.APP_DB_MAPPING.get(obj1._meta.app_label)
        db_obj2 = settings.APP_DB_MAPPING.get(obj2._meta.app_label)
        if db_obj1 and db_obj2:
            return db_obj1 == db_obj2
        # No hay una regla específica, delega en otros enrutadores o en el comportamiento predeterminado
        return None 

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """
        Asegura que las aplicaciones solo migren a la base de datos correcta.
        """
        if app_label in settings.APP_DB_MAPPING:
            # Si la aplicación tiene una asignación específica,
            # solo permite la migración si 'db' es esa base de datos.
            return settings.APP_DB_MAPPING[app_label] == db
        elif db == 'default':
            # Si la aplicación no tiene una asignación explícita,
            # permite que se migre a la base de datos 'default'.
            return True
        # Para cualquier otra base de datos, no permite la migración
        # si la aplicación no está explícitamente asignada.
        return False

Enrutamiento de Modelos Específicos Dentro de una Misma Aplicación

En ciertas situaciones, podría ser necesario que modelos individuales dentro de una misma aplicación residan en bases de datos diferentes. Para lograr esta granularidad, el enrutador debe adaptarse para tomar decisiones basadas en el nombre del modelo (accesible a través de model._meta.model_name) en lugar de depender únicamente de la etiqueta de la aplicación.

A continuación, se muestra cómo adaptar los métodos del enrutador para dirigir modelos específicos a bases de datos designadas. Es crucial recordar que si se utiliza este enfoque, la entrada en DATABASE_ROUTERS en settings.py debería apuntar a esta nueva clase de enrutador.

# mi_proyecto/routers.py (versión avanzada de enrutamiento por modelo)
from django.conf import settings

class ModeloDBRouter:
    """
    Un enrutador de base de datos que dirige las operaciones
    basándose en el nombre del modelo.
    """
    # Mapeo de nombres de modelos a sus bases de datos asignadas
    modelos_a_bases_datos = {
        'Producto': 'db_primaria',
        'Categoria': 'db_primaria',
        'LogAccion': 'db_secundaria',
        'HistoricoPrecio': 'db_secundaria',
    }

    def db_for_read(self, model, **hints):
        """
        Dirige las operaciones de lectura de modelos específicos a su base de datos asignada.
        """
        return self.modelos_a_bases_datos.get(model._meta.model_name, None)

    def db_for_write(self, model, **hints):
        """
        Dirige las operaciones de escritura de modelos específicos a su base de datos asignada.
        """
        return self.modelos_a_bases_datos.get(model._meta.model_name, None)

    def allow_relation(self, obj1, obj2, **hints):
        """
        Permite relaciones entre modelos si ambos están en la misma base de datos.
        """
        db_obj1 = self.modelos_a_bases_datos.get(obj1._meta.model_name)
        db_obj2 = self.modelos_a_bases_datos.get(obj2._meta.model_name)
        if db_obj1 and db_obj2:
            return db_obj1 == db_obj2
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """
        Asegura que los modelos solo migren a la base de datos correcta.
        """
        if model_name in self.modelos_a_bases_datos:
            # Si el modelo está mapeado, solo permite la migración si 'db' es su base de datos asignada.
            return self.modelos_a_bases_datos[model_name] == db
        # Si el modelo no está explícitamente mapeado aquí,
        # se puede delegar a otros enrutadores o prohibir la migración por defecto.
        # Por ejemplo, para evitar migraciones accidentales de modelos no mapeados:
        return False 

Etiquetas: Django BasesDeDatos MultiDB RoutersDjango ORMDjango

Publicado el 7-22 07:54