web-dev-qa-db-fra.com

Mise en cache/réutilisation d'une connexion à une base de données pour une utilisation ultérieure de la vue

Je sauvegarde la connexion à la base de données d'un utilisateur. La première fois qu'ils entrent dans leurs informations d'identification, je fais quelque chose comme ceci:

self.conn = MySQLdb.connect (
    Host = 'aaa',
    user = 'bbb',
    passwd = 'ccc',
    db = 'ddd',
    charset='utf8'
)
cursor = self.conn.cursor()
cursor.execute("SET NAMES utf8")
cursor.execute('SET CHARACTER SET utf8;')
cursor.execute('SET character_set_connection=utf8;')

J'ai alors le conn prêt à aller pour toutes les requêtes de l'utilisateur. Cependant, je ne veux pas me reconnecter chaque fois que la view est chargée. Comment pourrais-je stocker cette "connexion ouverte" afin que je puisse simplement faire quelque chose comme ceci dans la vue:

def do_queries(request, sql):
    user = request.user
    conn = request.session['conn']
    cursor = request.session['cursor']
    cursor.execute(sql)

Mise à jour : il semble que ce qui précède n’est pas possible et ne constitue pas une bonne pratique, alors laissez-moi reformuler ce que j’essaie de faire:

J'ai un éditeur SQL que l'utilisateur peut utiliser après avoir entré ses informations d'identification (pensez à quelque chose comme Navicat ou SequelPro). Notez que ceci estPASla connexion Django db par défaut - je ne connais pas les informations d’identité à l’avance. Maintenant, une fois que l'utilisateur est "connecté", j'aimerais qu'il puisse faire autant de requêtes qu'il le souhaite sans que je ne doive me reconnecter à chaque fois. Par exemple, pour répéter, quelque chose comme Navicat ou SequelPro. Comment cela pourrait-il être fait avec python, Django ou mysql? Peut-être que je ne comprends pas vraiment ce qui est nécessaire ici (mise en cache de la connexion? Pooling de connexion? Etc.), toute suggestion ou aide serait donc grandement appréciée.

6
David542

Vous pouvez utiliser un conteneur IoC pour stocker un fournisseur singleton pour vous. Essentiellement, au lieu de construire une nouvelle connexion à chaque fois, il ne la construira qu’une fois (la première fois que ConnectionContainer.connection_provider() est appelé), puis restituera toujours la connexion précédemment construite.

Vous aurez besoin du paquetage dependency-injector pour que mon exemple fonctionne:

import dependency_injector.containers as containers
import dependency_injector.providers as providers


class ConnectionProvider():
    def __init__(self, Host, user, passwd, db, charset):
        self.conn = MySQLdb.connect(
            Host=host,
            user=user,
            passwd=passwd,
            db=db,
            charset=charset
        )


class ConnectionContainer(containers.DeclarativeContainer):
    connection_provider = providers.Singleton(ConnectionProvider,
                                              Host='aaa',
                                              user='bbb',
                                              passwd='ccc',
                                              db='ddd',
                                              charset='utf8')


def do_queries(request, sql):
    user = request.user
    conn = ConnectionContainer.connection_provider().conn
    cursor = conn.cursor()
    cursor.execute(sql)

J'ai codé en dur la chaîne de connexion ici, mais il est également possible de la rendre variable en fonction d'une configuration modifiable. Dans ce cas, vous pouvez également créer un conteneur pour le fichier de configuration et demander au conteneur de connexion de lire sa configuration à partir de là. Vous définissez ensuite la configuration au moment de l'exécution. Comme suit:

import dependency_injector.containers as containers
import dependency_injector.providers as providers

class ConnectionProvider():
    def __init__(self, connection_config):
        self.conn = MySQLdb.connect(**connection_config)

class ConfigContainer(containers.DeclarativeContainer):
    connection_config = providers.Configuration("connection_config")

class ConnectionContainer(containers.DeclarativeContainer):
    connection_provider = providers.Singleton(ConnectionProvider, ConfigContainer.connection_config)

def do_queries(request, sql):
    user = request.user
    conn = ConnectionContainer.connection_provider().conn
    cursor = conn.cursor()
    cursor.execute(sql)


# run code
my_config = {
    'Host':'aaa',
    'user':'bbb',
    'passwd':'ccc',
    'db':'ddd',
    'charset':'utf8'
}

ConfigContainer.connection_config.override(my_config)
request = ...
sql = ...

do_queries(request, sql)
2
Karl

Je ne vois pas pourquoi vous avez besoin d'une connexion en cache ici et pourquoi ne pas simplement vous reconnecter à chaque demande mettant en cache les informations d'identification de l'utilisateur quelque part, mais je vais quand même essayer de vous présenter une solution qui pourrait répondre à vos besoins.

Je suggèrerais d’envisager d’abord une tâche plus générique - mettez en cache quelque chose entre les demandes ultérieures que votre application doit gérer et ne peut pas sérialiser dans les sessions de Django . Dans votre cas particulier, cette valeur partagée serait une connexion à la base de données). (ou plusieurs connexions) . Commençons par une tâche simple consistant à partager une simple variable compteur entre les requêtes, afin de comprendre ce qui se passe réellement sous le capot.

De manière générale, aucune des deux réponses n’a mentionné quoi que ce soit concernant un serveur Web que vous pourriez utiliser! En fait, il existe plusieurs façons de gérer les connexions simultanées dans les applications Web:

  1. Ayant plusieurs processus, chaque requête arrive dans un d'entre elles au hasard
  2. Ayant plusieurs threads, chaque requête est gérée par un thread aléatoire.
  3. p.1 et p.2 combinés
  4. Diverses techniques async, lorsqu'il y a un uniqueprocessus + boucle d'événement traitant des demandes avec un avertissement que les gestionnaires de demandes ne doivent pas bloquer pendant longtemps

D'après ma propre expérience, les p.1-2 conviennent à la majorité des applications Web typiquesApache1.x ne peut fonctionner qu'avec p.1, Apache2.x peut gérer les 1-3.

Commençons par l'application Django suivante et exécutons un processus unique gunicorn webserver . Je vais utiliser gunicorn car il est assez facile de le configurer contrairement à Apache (opinion personnelle :-)

views.py

import time

from Django.http import HttpResponse

c = 0

def main(self):
    global c
    c += 1
    return HttpResponse('val: {}\n'.format(c))


def heavy(self):
    time.sleep(10)
    return HttpResponse('heavy done')

urls.py

from Django.contrib import admin
from Django.urls import path

from . import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.main, name='main'),
    path('heavy/', views.heavy, name='heavy')
]

L'exécuter dans un seul mode de traitement:

gunicorn testpool.wsgi -w 1

Voici notre arbre de processus - il n'y a qu'un seul travailleur capable de traiter TOUTES les demandes.

pstree 77292
-+= 77292 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 1
 \--- 77295 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 1

Essayer d'utiliser notre application:

curl 'http://127.0.0.1:8000'
val: 1

curl 'http://127.0.0.1:8000'
val: 2

curl 'http://127.0.0.1:8000'
val: 3

Comme vous pouvez le constater, vous pouvez facilement partager le compteur entre les demandes suivantes ... Le problème ici est que vous ne pouvez traiter qu’une seule demande en parallèle. Si vous demandez / heavy/ dans un onglet, / ne fonctionnera pas tant que / heavy ne sera pas terminé

Permet maintenant d'utiliser 2 processus de travail:

gunicorn testpool.wsgi -w 2

Voici à quoi ressemblerait l’arbre de processus:

 pstree 77285
-+= 77285 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 2
 |--- 77288 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 2
 \--- 77289 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 2

Test de notre application:

curl 'http://127.0.0.1:8000'
val: 1

curl 'http://127.0.0.1:8000'
val: 2

curl 'http://127.0.0.1:8000'
val: 1

Les deux premières demandes ont été traitées par le premier worker process et la 3ème - par le deuxième processus de travail disposant de son propre espace mémoire, de sorte que vous voyez 1 au lieu de 3 ..__ Notez que votre sortie peut différer car les processus 1 et 2 sont sélectionnés de manière aléatoire. Mais tôt ou tard, vous lancerez un processus différent.

Ce n'est pas très utile pour nous car nous devons traiter plusieurs} demandes simultanées et nous devons d'une manière ou d'une autre faire traiter notre demande par un processus spécifique qui ne peut pas être effectué dans le cas général.

La plupart des regroupements techniques qui sortent de la boîte ne mettraient en cache que les connexions entrant dans le cadre d'un processus unique, si votre demande est traitée par un processus différent - un NOUVEAU la connexion doit être faite.

Permet de passer aux discussions

gunicorn testpool.wsgi -w 1 --threads 2

Encore une fois - seulement 1 processus

pstree 77310
-+= 77310 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 1 --threads 2
 \--- 77313 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 1 --threads 2

Maintenant, si vous exécutez / heavy dans un onglet, vous pourrez toujours interroger / et votre compteur sera préservé entre les requêtes! Même si le nombre de threads augmente ou diminue en fonction de votre charge de travail, il devrait quand même fonctionner correctement.

Problems: vous aurez besoin de synchroniser accéder à la variable partagée comme ceci en utilisant les techniques de synchronisation des threads python ( en savoir plus ) . Un autre problème est que le même utilisateur peut besoin d'émettre plusieurs requêtes en parallèle, c'est-à-dire d'ouvrir plusieurs onglets.

Pour le gérer, vous pouvez ouvrir plusieurs connexions sur la première demande lorsque vous disposez des informations d'identification de base de données.

Si un utilisateur a besoin de plus de connexions que votre application risque de rester bloqué jusqu'à ce qu'une connexion soit disponible.

Retour à votre question

Vous pouvez créer une classe qui aurait les méthodes suivantes:

from contextlib import contextmanager

class ConnectionPool(object):

   def __init__(self, max_connections=4):
      self._pool = dict()
      self._max_connections = max_connections

   def preconnect(self, session_id, user, password):
       # create multiple connections and put them into self._pool
       # ...

    @contextmanager
    def get_connection(sef, session_id):
       # if have an available connection:
            # mark it as allocated
            # and return it
            try:
                yield connection
           finally:
              # put it back to the pool
              # ....
       # else
        # wait until there's a connection returned to the pool by another thread

pool = ConnectionPool(4)

def some_view(self):
     session_id = ...
     with pool.get_connection(session_id) as conn:
        conn.query(...)

Ce n'est pas une solution complète - vous devrez en quelque sorte supprimer les connexions obsolètes non utilisées pendant longtemps.

Si un utilisateur revient après un long moment et que sa connexion a été fermée, il devra fournir à nouveau ses informations d'identification. J'espère que cela vous convient du point de vue de votre application.

Gardez également à l'esprit que python threads a ses pénalités de performance, ne sachant pas si cela vous pose problème.

Je ne l'ai pas vérifié pour Apache2 (trop de configuration, je ne l'utilise pas depuis très longtemps et j'utilise généralement uwsgi ), mais cela devrait également fonctionner là-bas - je serais heureux de recevoir vos réponses. si vous parvenez à l'exécuter)

Et n'oubliez pas aussi p.4 (approche asynchrone) - vous ne pourrez probablement pas l’utiliser sur Apache, mais cela vaut la peine d’être examiné - mots-clés: Django + gevent, Django + asyncio. Il a ses avantages et ses inconvénients et peut grandement affecter la mise en œuvre de votre application. Il est donc difficile de proposer une solution sans connaître en détail les exigences de votre application.

1
ffeast

Ce n'est pas une bonne idée de faire une telle chose de manière synchrone dans le contexte d'applications Web. N'oubliez pas que votre application doit parfois fonctionner en mode multi processus/thread et que vous ne pouvez pas partager la connexion entre les processus normalement. Donc, si vous créez une connexion pour votre utilisateur sur un processus, il n'y a aucune garantie de recevoir une requête sur le même. Une meilleure idée serait peut-être d’avoir un seul ouvrier en arrière-plan qui gérera les connexions dans plusieurs threads (un thread par session) pour effectuer des requêtes sur la base de données et récupérer le résultat sur une application Web. Votre application doit attribuer un identifiant unique à chaque session et l'agent en arrière-plan suit chaque thread à l'aide de l'identifiant de session. Vous pouvez utiliser celery ou toute autre file de tâches prenant en charge le résultat asynchrone. Donc, la conception serait quelque chose comme ci-dessous:

             |<--|        |<--------------|                   |<--|
user (id: x) |   | webapp |   | queue |   | worker (thread x) |   | DB
             |-->|        |-->|       |-->|                   |-->|

Vous pouvez également créer une file d'attente pour chaque utilisateur jusqu'à ce qu'il ait une session active. Vous pouvez ainsi exécuter un processus en arrière-plan distinct pour chaque session.

1
sharez

En fait, j'ai partagé ma solution à ce problème précis. Ce que j'ai fait ici était de créer un pool de connexions que vous pouvez spécifier avec max, puis de placer les requêtes en file d'attente de manière asynchrone via ce canal. De cette façon, vous pouvez laisser un certain nombre de connexions ouvertes, mais la file d'attente et le pool seront asynchrones tout en conservant la vitesse habituelle. 

Cela nécessite gevent et postgres.

Python Postgres psycopg2 ThreadedConnectionPool épuisé

1
eatmeimadanish

Je partage juste mes connaissances ici.

Installez PyMySQL pour utiliser MySql} _

Pour Python 2.x

pip install PyMySQL

Pour Python 3.x

pip3 install PyMySQL

1. Si vous êtes prêt à utiliser Django Framework, il est très facile de lancer la requête SQL sans aucune reconnexion.

Dans le fichier setting.py, ajoutez les lignes ci-dessous

DATABASES = {
        'default': {
            'ENGINE': 'Django.db.backends.mysql',
            'NAME': 'test',
            'USER': 'test',
            'PASSWORD': 'test',
            'Host': 'localhost',
            'OPTIONS': {'charset': 'utf8mb4'},
        }
    }

Dans le fichier views.py, ajoutez ces lignes pour obtenir les données. Vous pouvez personnaliser votre requête en fonction de vos besoins

from Django.db import connection
def connect(request):
    cursor = connection.cursor()
    cursor.execute("SELECT * FROM Tablename");
    results = cursor.fetchall()
    return results 

Vous obtiendrez les résultats souhaités.

Cliquez ici pour plus d'informations à ce sujet 

2. Pour Python Tkinter

from Tkinter import *
import MySQLdb

db = MySQLdb.connect("localhost","root","root","test")
# prepare a cursor object using cursor() method
cursor = db.cursor()
cursor.execute("SELECT * FROM Tablename")
if cursor.fetchone() is not None:
    print("In If")
else:
    print("In Else")
cursor.close()

Reportez-vous this pour plus d'informations 

PS: Vous pouvez vérifier ce lien pour votre question concernant la réutilisation d'une connexion à une base de données pour plus tard. 

Comment activer la reconnexion automatique du client MySQL avec MySQLdb?

0
Anoop Kumar

Je ne suis pas un expert dans ce domaine, mais je pense que PgBouncer ferait le travail à votre place, en supposant que vous puissiez utiliser un back-end PostgreSQL (c'est un détail que vous n'avez pas précisé). PgBouncer est un pooler de connexion, qui vous permet de réutiliser des connexions en évitant la surcharge liée à la connexion à chaque demande.

Selon leur documentation :

mot de passe de l'utilisateur

Si user = est défini, toutes les connexions à la base de données de destination seront effectuées avec l'utilisateur spécifié, ce qui signifie qu'il n'y aura qu'un seul pool pour cette base de données.

Sinon, PgBouncer essaie de se connecter à la base de données de destination avec le nom d'utilisateur du client, ce qui signifie qu'il y aura un pool par utilisateur.

Ainsi, vous pouvez avoir un seul pool de connexions par utilisateur, ce qui ressemble à ce que vous voulez.

Dans MySQL Land, le module mysql.connector.pool vous permet d'effectuer un regroupement de connexions, bien que je ne sache pas si vous pouvez réaliser un regroupement par utilisateur. Étant donné que vous pouvez configurer le nom du pool, j'imagine que vous pouvez utiliser le nom de l'utilisateur pour identifier le pool.

Indépendamment de ce que vous utilisez, vous aurez probablement des occasions où la reconnexion est inévitable (un utilisateur se connecte, fait un certain nombre de choses, s'absente pour une réunion ou un déjeuner, revient et souhaite prendre davantage de mesures).

0
Jonah Bishop