Jag har gjort något liknande som ligger närmast punkt 1, men istället för att använda middleware för att ställa in en standardanslutning används Django-databasroutrar. Detta tillåter applikationslogik att använda ett antal databaser om det krävs för varje begäran. Det är upp till applikationslogiken att välja en lämplig databas för varje fråga, och detta är den stora nackdelen med detta tillvägagångssätt.
Med denna inställning listas alla databaser i settings.DATABASES
, inklusive databaser som kan delas mellan kunder. Varje modell som är kundspecifik placeras i en Django-app som har en specifik appetikett.
t.ex. Följande klass definierar en modell som finns i alla kunddatabaser.
class MyModel(Model):
....
class Meta:
app_label = 'customer_records'
managed = False
En databasrouter placeras i settings.DATABASE_ROUTERS
kedja för att dirigera databasbegäran med app_label
, något sånt här (inte ett fullständigt exempel):
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)
Den speciella delen med denna router är thread_local_data.current_customer_db()
ringa upp. Innan routern tränas måste den som ringer/applikationen ha ställt in den aktuella kunddb i thread_local_data
. En Python-kontexthanterare kan användas för detta ändamål för att pusha/poppa en aktuell kunddatabas.
Med allt detta konfigurerat ser applikationskoden sedan ut ungefär så här, där UseCustomerDatabase
är en kontexthanterare för att pusha/poppa ett aktuellt kunddatabasnamn till thread_local_data
så att thread_local_data.current_customer_db()
returnerar korrekt databasnamn när routern så småningom träffas:
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)
Detta är redan en ganska komplicerad installation. Det fungerar, men jag ska försöka sammanfatta vad jag ser som fördelar och nackdelar:
Fördelar
- Databasvalet är flexibelt. Det gör att flera databaser kan användas i en enda fråga, både kundspecifika och delade databaser kan användas i en förfrågan.
- Databasvalet är explicit (osäker på om detta är en fördel eller nackdel). Om du försöker köra en fråga som träffar en kunddatabas men applikationen inte har valt en, kommer ett undantag att inträffa som indikerar ett programmeringsfel.
- Om du använder en databasrouter kan olika databaser existera på olika värdar istället för att förlita sig på en
USE db;
uttalande som gissar att alla databaser är tillgängliga via en enda anslutning.
Nackdelar
- Det är komplicerat att installera, och det finns en hel del lager inblandade för att få det att fungera.
- Behovet och användningen av lokala tråddata är oklara.
- Vyer är fyllda med databasvalskod. Detta skulle kunna abstraheras med hjälp av klassbaserade vyer för att automatiskt välja en databas baserat på förfrågningsparametrar på samma sätt som mellanprogram skulle välja en standarddatabas.
- Kontexthanteraren för att välja en databas måste lindas runt en frågeuppsättning på ett sådant sätt att kontexthanteraren fortfarande är aktiv när frågan utvärderas.
Förslag
Om du vill ha flexibel databasåtkomst, skulle jag föreslå att du använder Djangos databasroutrar. Använd Middleware eller en vy Mixin som automatiskt ställer in en standarddatabas att använda för anslutningen baserat på begärande parametrar. Du kanske måste ta till lokala tråddata för att lagra standarddatabasen som ska användas så att när routern träffas vet den vilken databas den ska dirigera till. Detta tillåter Django att använda sina befintliga beständiga anslutningar till en databas (som kan finnas på olika värdar om så önskas), och väljer databasen att använda baserat på routing som ställts in i begäran.
Detta tillvägagångssätt har också fördelen att databasen för en fråga kan åsidosättas om det behövs genom att använda QuerySet using()
funktion för att välja en annan databas än standard.