Applications multi-locataires Django: modification de la connexion à la base de données par demande?

je suis à la recherche de code de travail et des idées d'autres qui ont essayé de construire une application multi-locataires Django en utilisant l'isolation de base de données.

mise à Jour/Solution: j'ai fini la résolution de ce dans un nouveau projet opensource: voir django-db-multitenant

Objectif

mon but est de multiplexer les requêtes quand elles arrivent sur un seul serveur app (WSGI frontend comme gunicorn), basé sur le nom d'hôte de la requête ou le chemin de la requête (par exemple, foo.example.com/ définit la connexion Django pour utiliser la base de données foo , et bar.example.com/ utilise la base de données bar ).

précédent

je suis au courant de quelques solutions existantes pour la multi-location à Django:

  1. django-tenant-schemas : c'est très proche de ce que je veux: vous installez son middleware au plus haut niveau, et il envoie une commande SET search_path au db. Malheureusement, il est Postgres spécifique et je suis coincé avec MySQL.
  2. django-simple-multiténant : la stratégie ici est d'ajouter une clé étrangère "locataire" à tous les modèles, et ajuster toute la logique d'entreprise d'application à la touche off de cela. En gros, chaque ligne est indexée par (id, tenant_id) plutôt que (id) . J'ai essayé, et je n'aime pas cette approche pour un certain nombre de raisons: cela rend l'application plus complexe, il peut conduire à des bogues difficiles à trouver, et il ne fournit pas d'isolation au niveau de la base de données.
  3. un {serveur d'Application, fichier de configuration django avec la db appropriée} par locataire. Aka La multi-tenance de l'homme pauvre (en fait de l'homme riche, compte tenu des ressources qu'elle implique). Je ne veux pas lancer un nouveau serveur d'application par le locataire, et de l'évolutivité je veux tout serveur d'application pour être en mesure d'expédier les demandes de n'importe quel client.

idées

Ma meilleure idée jusqu'à présent est de faire quelque chose comme django-tenant-schemas : dans le premier middleware, saisir django.db.connection et jouer avec la sélection de base de données plutôt que le schéma. Je n'ai pas bien réfléchi à ce que cela signifie en termes de connexions mises en commun / persistantes

un autre cul-de-sac que j'ai poursuivi était les préfixes de table spécifiques aux locataires: mis à part que j'aurais besoin d'eux pour être dynamique, même un préfixe de table global n'est pas facilement atteint à Django (voir billet rejeté 5000 , entre autres).

enfin, Django prise en charge de plusieurs bases de données permet de définir plusieurs bases de données nommées, et mux parmi elles en fonction du type d'instance et du mode Lecture/écriture. Pas utile puisqu'il n'y a pas de possibilité de sélectionner le pb sur une base par demande.

Question

Quelqu'un a-t-il réussi quelque chose de semblable? Dans l'affirmative, comment l'avez-vous Mise en œuvre?

30
demandé sur mik3y 2013-05-23 22:13:24

3 réponses

j'ai fait quelque chose de similaire qui est le plus proche du point 1, mais au lieu d'utiliser middleware pour définir une connexion par défaut les routeurs de base de données Django sont utilisés. Cela permet à application logic d'utiliser un certain nombre de bases de données si nécessaire pour chaque requête. C'est à la demande de la logique de choisir une base de données pour chaque requête, et c'est le gros inconvénient de cette approche.

avec cette configuration, toutes les bases de données sont listées dans settings.DATABASES , y compris les bases de données qui peut être partagé entre les clients. Chaque modèle personnalisé est placé dans une application Django qui possède une étiquette app spécifique.

par exemple. La classe suivante définit un modèle qui existe dans toutes les bases de données client.

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

un routeur de base de données est placé dans la chaîne settings.DATABASE_ROUTERS pour acheminer la demande de base de données par app_label , quelque chose comme ceci (pas un exemple complet):

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 partie spéciale sur ce routeur est l'appel thread_local_data.current_customer_db() . Avant que le routeur soit utilisé, l'appelant/l'application doit avoir configuré la base de données client actuelle dans thread_local_data . Un gestionnaire de contexte Python peut être utilisé à cette fin pour pousser/pop une base de données client actuelle.

avec tout cela configuré, le code d'application ressemble alors à quelque chose comme ceci, où UseCustomerDatabase est un gestionnaire de contexte pour pousser / pop un nom de base de données client actuel dans thread_local_data de sorte que thread_local_data.current_customer_db() retournera le corriger le nom de la base de données lorsque le routeur est éventuellement activé:

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)

c'est déjà une configuration assez complexe. Cela fonctionne, mais je vais essayer de résumer ce que je vois comme avantages et inconvénients:

avantages

  • la sélection de la base de données est flexible. Il permet d'utiliser plusieurs bases de données dans une seule requête, les bases de données spécifiques au client et partagées peuvent être utilisées dans une requête.
  • La sélection de la base de données
  • est explicite (on ne sait pas s'il s'agit d'un avantage ou d'un inconvénient). Si vous essayez d'exécuter une requête qui frappe une base de données de client mais l'application n'a pas sélectionné un, une exception se produira indiquant une erreur de programmation.
  • L'utilisation d'un routeur de base de données permet à différentes bases de données d'exister sur des hôtes différents, plutôt que de s'appuyer sur une déclaration USE db; qui suppose que toutes les bases de données sont accessibles par une connexion unique.

désavantages

  • c'est complexe à configurer, et il y a pas mal de couches impliquées pour le faire fonctionner.
  • le besoin et l'utilisation de données locales de thread est obscur.
  • vues sont parsemées de code de sélection de base de données. Cela pourrait être résumé en utilisant des vues basées sur la classe pour choisir automatiquement une base de données basée sur les paramètres de la requête dans le même manner comme middleware choisirait une base de données par défaut.
  • le gestionnaire de contexte pour choisir une base de données doit être enroulé autour d'un queryset de telle manière que le gestionnaire de contexte est toujours actif lorsque la requête est évaluée.

Suggestions

si vous voulez un accès flexible à la base de données, je vous conseille D'utiliser les routeurs de la base de données de Django. Utilisez Middleware ou une vue Mixin qui s'installe automatiquement une base de données par défaut à utiliser pour la connexion basée sur les paramètres de la requête. Vous pourriez avoir à recourir à thread local database pour stocker la base de données par défaut à utiliser de sorte que lorsque le routeur est frappé, il sait à quelle base de données acheminer. Cela permet à Django d'utiliser ses connexions persistantes existantes à une base de données (qui peut résider sur différents serveurs si désiré), et choisit la base de données à utiliser sur la base du routage mis en place dans la demande.

Cette approche a aussi l'avantage que la base de données pour une requête peut être annulée si nécessaire en utilisant la fonction QuerySet using() pour sélectionner une base de données autre que la valeur par défaut.

14
répondu Austin Phillips 2013-06-06 13:01:20

vous pouvez créer un middleware simple de votre propre qui a déterminé le nom de la base de données à partir de votre sous-domaine ou autre et puis exécuté un utiliser déclaration sur le curseur de la base de données pour chaque demande. En regardant le code django-tenants-schema, c'est essentiellement ce qu'il fait. Il est sous-classant psycopg2 et émet les postgres équivalents à utiliser, "set search_path XXX". Vous pouvez créer un modèle pour gérer et créer vos locataires trop, mais alors vous seriez en réécriture de la plupart des Django-locataires-schéma.

il ne devrait pas y avoir de pénalité de performance ou de ressources dans MySQL à la commutation du schéma (nom de la base de données). Il s'agit simplement de définir un paramètre de session pour la connexion.

2
répondu Victor Bruno 2013-05-31 01:02:51

pour mémoire, j'ai choisi de mettre en œuvre une variante de ma première idée: publier un USE <dbname> dans un middleware de requête anticipée. J'ai aussi configuré le préfixe CACHE de la même façon.

Je l'utilise sur un petit site de production, cherchant le nom du locataire à partir d'une base de données Redis basée sur l'hôte de demande. Jusqu'à présent, je suis très satisfait des résultats.

j'en ai fait un projet github ici: https://github.com/mik3y/django-db-multitenant

2
répondu mik3y 2013-07-11 17:58:52