Manager para diferentes conexões de banco no Django

Pra tirar um pouco as teias de aranha do blog, vou escrever sobre algo que fiz recentemente e achei bacana.

Eu estava trabalhando em um novo projeto (em Django, é claro), vinculado a um projeto principal e comecei a me sentir um pouco amarrado. Os dois projetos compartilhavam as mesmas aplicações mas depois de algumas definições no meio do caminho pode-se ver que eles precisariam compartilhar pouquíssimo conteúdo.

Há algum tempo, havia visto um manager que era capaz de se conectar a um banco diferente do padrão e achei que esta seria a solução ideal. Eu poderia separar o conteúdo dos dois projetos e, quando precisasse, usaria o banco de dados do outro projeto.

O manager que eu havia visto estava em algum destes projetos em Django que foram abertos ao público recentemente, mas não consegui mais encontrar. Então me vi obrigado a fazer um pouco de pesquisa e inventar o meu próprio.

Acabei encontrando uma solução do Eric Florenzano e outra do Kenneth Falck.

A do Erick parecia um tanto antiga e junto com a pesquisa esbarrei na changeset 10026, em que foi alterada a maneira de se conectar ao banco, ao invés de usar o módulo django.conf.settings o parâmetro passa a ser um dicionário com dados de conexão, e isso facilitou bastante.

Chega de papo furado e vamos ao código. Vou explicando por partes.

Primeiro foi preciso definir uma maneira fácil de usar conexões diferentes sem ficar repetindo no código as configurações de cada conexão. A solução copiada do Eric foi criar um dicionário de conexões nas configurações do projeto:

# Configurações padrão do banco
DATABASE_ENGINE = 'postgresql_psycopg2'
DATABASE_NAME = 'default_db_name'
DATABASE_USER = 'user'
DATABASE_PASSWORD = ''
DATABASE_HOST = ''
DATABASE_PORT = ''

# Configuração padrão + alternativas
DATABASES = {
    'default': {},
    'alternative': {
        'DATABASE_NAME': 'alternative_db_name',
    },
}

Como eu disse, quiz deixar fácil o uso de conexões diferentes, então fiz com que não fosse necessário repetir os dados de configuração de cada conexão, visto que é comum que se use o mesmo usuário, senha, engine, etc. Então se você pretende somente usar outro banco, basta mudar o DATABASE_NAME. Abaixo o código responsável por isto:

from django.conf import settings
from django.db import models, backend

# Dicionário de conexão padrão
db_settings = {
    'DATABASE_HOST': settings.DATABASE_HOST,
    'DATABASE_NAME': settings.DATABASE_NAME,
    'DATABASE_OPTIONS': settings.DATABASE_OPTIONS,
    'DATABASE_PASSWORD': settings.DATABASE_PASSWORD,
    'DATABASE_PORT': settings.DATABASE_PORT,
    'DATABASE_USER': settings.DATABASE_USER,
    'TIME_ZONE': settings.TIME_ZONE,
}

def prepare_db_settings(db_profile_name):
    """
    Recebe um dicionário de conexão alternativa e retorna um novo dicionário,
    completando propriedades ausentes com os valores padrão.
    """
    return dict(db_settings, **settings.DATABASES[db_profile_name])

Por fim, o manager se comporta normalmente, se você quiser conectar a um banco diferente é só usar o método use, informando o nome do perfil de conexão que deseja utilizar. O manager retorna a query padrão mas aponta para outro banco de dados. Bacana não?

class MultiDBManager(models.Manager):
    """
    A manager that can connect to different databases.
    """
    def use(self, db_profile_name):
    	"""
    	Return a queryset connected to a custom database.
    	"""
        # Get customized database settings to use in a new connection wrapper
        db_settings = prepare_db_settings(db_profile_name)

    	# Get the queryset and replace its connection
        qs = self.get_query_set()
        qs.query.connection = backend.DatabaseWrapper(db_settings)
        return qs

Mas nem tudo são flores, é tudo muito experimental e não foi testado a fundo. Aqui vão alguns problemas conhecidos:

  1. Você vai encontrar erros se tentar acessar models relacionados, o manager inicial vai usar a conexão alternativa, mas o model relacionado vai ser procurado no banco padrão. Para contornar isto use o método select_related(‘model_relacionado’), desta forma todos os models serão listados em apenas uma consulta ao mesmo banco.
  2. Não foi testado nenhum tipo de alteração de dados (inclusão, edição, exclusão). O post do Eric talvez possa ajudar a encontrar uma solução neste sentido.

De qualquer forma, tem sido bastante útil para integrar projetos sem fazer muita bagunça.

E você? O que achou? Deixe sua opinião nos comentários!

* Disponibilizei o código no Django Snippets também: Manager for multiple database connections.

Herança no Django (Model Inheritance)

Recentemente estava trabalhando com herança de modelos no Django (model inheritance para os estrangeiros) e precisei saber, à partir da classe mãe, qual era a classe filha.

Até então não tinha me preocupado como funcionam as “entranhas” do Django pra dar suporte à herança de modelos, é interessante.

Os dados da classe mãe são salvos em uma tabela, e os das filhas em outra, uma nova tabela para cada filha. Quando é carregada uma instância da classe filha, ela traz consigo os dados herdados da mãe (através de um JOIN no banco). Já a mãe pode ser chamada de desnaturada, não é muito boa para achar suas filhas.

Então o que eu fiz foi adicionar uma relação genérica à classe mãe e alterar o método save para relacionar à classe filha apenas gravando o content-type, assim eu consegui fazer a família se comunicar melhor.

Depois que postei no Django Snippets vi que mais gente tinha feito algo parecido, mas eu juro que só vi depois! 🙂

De qualquer forma, aí vai:

from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

class Place(models.Model):
    name = models.CharField('name')
    content_type = models.ForeignKey(ContentType)
    # Child model
    child = generic.GenericForeignKey(fk_field='id')

    def save(self, **kwargs):
        if not self.pk:
            self.content_type = ContentType.objects.get_for_model(self)
        super(Place, self).save(**kwargs)

class Restaurant(Place):
    pass

Ou veja no Django Snippets, lá fica mais bonito: Child aware model inheritance.

Tem algumas variações aqui.