Mysql
 sql >> Datenbank >  >> RDS >> Mysql

Mandantenfähige Django-Anwendungen:Ändern der Datenbankverbindung pro Anfrage?

Ich habe etwas Ähnliches getan, das Punkt 1 am nächsten kommt, aber anstatt Middleware zum Festlegen einer Standardverbindung zu verwenden, werden Django-Datenbankrouter verwendet. Dadurch kann die Anwendungslogik bei Bedarf für jede Anforderung eine Reihe von Datenbanken verwenden. Es liegt an der Anwendungslogik, für jede Abfrage eine geeignete Datenbank auszuwählen, und das ist der große Nachteil dieses Ansatzes.

Bei diesem Setup werden alle Datenbanken in settings.DATABASES aufgelistet , einschließlich Datenbanken, die von Kunden gemeinsam genutzt werden können. Jedes kundenspezifische Modell wird in einer Django-App platziert, die ein bestimmtes App-Label hat.

z.B. Die folgende Klasse definiert ein Modell, das in allen Kundendatenbanken vorhanden ist.

class MyModel(Model):
    ....
    class Meta:
        app_label = 'customer_records'
        managed = False

Ein Datenbank-Router wird in den settings.DATABASE_ROUTERS platziert Kette zum Weiterleiten von Datenbankanfragen durch app_label , etwa so (kein vollständiges Beispiel):

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)

Das Besondere an diesem Router ist der thread_local_data.current_customer_db() Forderung. Bevor der Router ausgeführt wird, muss der Aufrufer/die Anwendung die aktuelle Kundendatenbank in thread_local_data eingerichtet haben . Zu diesem Zweck kann ein Python-Kontextmanager verwendet werden, um eine aktuelle Kundendatenbank zu pushen/poppen.

Wenn all dies konfiguriert ist, sieht der Anwendungscode dann etwa so aus, wobei UseCustomerDatabase ist ein Kontextmanager, um einen aktuellen Kundendatenbanknamen in thread_local_data einzufügen damit thread_local_data.current_customer_db() wird den korrekten Datenbanknamen zurückgeben, wenn der Router schließlich getroffen wird:

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)

Das ist schon ein recht komplexes Setup. Es funktioniert, aber ich werde versuchen zusammenzufassen, was ich als Vor- und Nachteile sehe:

Vorteile

  • Die Datenbankauswahl ist flexibel. Es ermöglicht die Verwendung mehrerer Datenbanken in einer einzigen Abfrage, sowohl kundenspezifische als auch gemeinsam genutzte Datenbanken können in einer Anfrage verwendet werden.
  • Die Datenbankauswahl ist explizit (nicht sicher, ob dies ein Vor- oder Nachteil ist). Wenn Sie versuchen, eine Abfrage auszuführen, die auf eine Kundendatenbank trifft, die Anwendung jedoch keine ausgewählt hat, tritt eine Ausnahme auf, die auf einen Programmierfehler hinweist.
  • Die Verwendung eines Datenbankrouters ermöglicht es, dass verschiedene Datenbanken auf verschiedenen Hosts existieren, anstatt sich auf eine USE db; zu verlassen -Anweisung, die davon ausgeht, dass auf alle Datenbanken über eine einzige Verbindung zugegriffen werden kann.

Nachteile

  • Es ist komplex einzurichten und es sind einige Ebenen erforderlich, damit es funktioniert.
  • Die Notwendigkeit und Verwendung von Thread-lokalen Daten ist unklar.
  • Ansichten sind mit Datenbankauswahlcode übersät. Dies könnte mit klassenbasierten Ansichten abstrahiert werden, um automatisch eine Datenbank basierend auf Anforderungsparametern auszuwählen, so wie Middleware eine Standarddatenbank auswählen würde.
  • Der Kontextmanager zur Auswahl einer Datenbank muss so um einen Abfragesatz gewickelt werden, dass der Kontextmanager immer noch aktiv ist, wenn die Abfrage ausgewertet wird.

Vorschläge

Wenn Sie einen flexiblen Datenbankzugriff wünschen, würde ich vorschlagen, die Datenbankrouter von Django zu verwenden. Verwenden Sie Middleware oder ein View-Mixin, das basierend auf Anforderungsparametern automatisch eine Standarddatenbank für die Verbindung einrichtet. Möglicherweise müssen Sie auf lokale Thread-Daten zurückgreifen, um die zu verwendende Standarddatenbank zu speichern, damit der Router weiß, an welche Datenbank er weitergeleitet werden soll, wenn er getroffen wird. Dadurch kann Django seine bestehenden dauerhaften Verbindungen zu einer Datenbank verwenden (die sich auf Wunsch auf verschiedenen Hosts befinden kann) und die zu verwendende Datenbank basierend auf dem in der Anfrage eingerichteten Routing auswählen.

Dieser Ansatz hat auch den Vorteil, dass die Datenbank für eine Abfrage bei Bedarf überschrieben werden kann, indem der QuerySet using() Funktion, um eine andere Datenbank als die Standarddatenbank auszuwählen.