web-dev-qa-db-fra.com

Définition du schéma pour toutes les requêtes d'une connexion dans psycopg2: Obtention de la condition de concurrence lors de la définition de search_path

Notre système fonctionne sur Ubuntu, python 3.4, postgres 9.4.x et psycopg2.

Nous (serons dans le futur) répartis entre les environnements dev, test et prod en utilisant des schémas. J'ai créé une méthode pratique pour créer des connexions à notre base de données. Il utilise des fichiers de configuration de connexion json pour créer la chaîne de connexion. Je souhaite configurer la connexion pour utiliser un schéma particulier pour toutes les requêtes suivantes à l'aide de la connexion renvoyée. Je ne veux pas que mes requêtes aient des schémas codés en dur, car nous devrions pouvoir basculer facilement entre eux selon que nous sommes en phase de développement, de test ou de production/environnement.

Actuellement, la méthode pratique ressemble à ceci:

def connect(conn_config_file = 'Commons/config/conn_commons.json'):
    with open(conn_config_file) as config_file:    
        conn_config = json.load(config_file)

    conn = psycopg2.connect(
        "dbname='" + conn_config['dbname'] + "' " +
        "user='" + conn_config['user'] + "' " +
        "Host='" + conn_config['Host'] + "' " +
        "password='" + conn_config['password'] + "' " +
        "port=" + conn_config['port'] + " "
    )
    cur = conn.cursor()
    cur.execute("SET search_path TO " + conn_config['schema'])

    return conn

Cela fonctionne très bien tant que vous lui donnez le temps d'exécuter la requête set search_path. Malheureusement, si je suis trop rapide avec l'exécution d'une requête suivante, une condition de concurrence critique se produit lorsque le search_path N'est pas défini. J'ai essayé de forcer l'exécution en faisant une conn.commit() avant le return conn, Cependant, cela réinitialise le search_path Au schéma par défaut postgres afin que il n'utilise pas, par exemple, prod. Les suggestions au niveau de la base de données ou de l'application sont préférables, cependant, je sais que nous pourrions probablement résoudre cela au niveau du système d'exploitation également, toutes les suggestions dans ce sens sont également les bienvenues.

Un exemple de fichier de configuration json ressemble à ceci:

{
    "dbname": "thedatabase",
    "user": "theuser",
    "Host": "localhost",
    "password": "theusers_secret_password",
    "port": "6432",
    "schema": "prod"
}

Toute suggestion est très appréciée.

15
André C. Andersen

Je pense qu'une solution plus élégante serait de définir le paramètre search_path Dans options de connect(), comme ceci:

def connect(conn_config_file = 'Commons/config/conn_commons.json'):
    with open(conn_config_file) as config_file:    
        conn_config = json.load(config_file)

    schema = conn_config['schema']
    conn = psycopg2.connect(
        dbname=conn_config['dbname'],
        user=conn_config['user'],
        Host=conn_config['Host'],
        password=conn_config['password'],
        port=conn_config['port'],
        options=f'-c search_path={schema}',
    )
    return conn

Bien sûr, vous pouvez utiliser des "options" dans le cadre de la chaîne de connexion. Mais l'utilisation d'arguments de mots clés évite tout problème avec les concaténations de chaînes.

J'ai trouvé cette solution dans cette demande de fonctionnalité psycopg2 . Quant au paramètre "options" lui-même, il est mentionné ici .

19
butla

Je pense qu'une meilleure idée est d'avoir quelque chose comme DatabaseCursor renvoyant le curseur que vous utilisez pour exécuter des requêtes avec "SET search_path ..." au lieu de la connexion. Eh bien, je veux dire quelque chose comme ça:

class DatabaseCursor(object):

    def __init__(self, conn_config_file):
        with open(conn_config_file) as config_file:     
            self.conn_config = json.load(config_file)

    def __enter__(self):
        self.conn = psycopg2.connect(
            "dbname='" + self.conn_config['dbname'] + "' " + 
            "user='" + self.conn_config['user'] + "' " + 
            "Host='" + self.conn_config['Host'] + "' " + 
            "password='" + self.conn_config['password'] + "' " + 
            "port=" + self.conn_config['port'] + " " 
        )   
        self.cur = self.conn.cursor()
        self.cur.execute("SET search_path TO " + self.conn_config['schema'])

        return self.cur

    def __exit__(self, exc_type, exc_val, exc_tb):
        # some logic to commit/rollback
        self.conn.close()

et

with DatabaseCursor('Commons/config/conn_commons.json') as cur:
    cur.execute("...")
7
bersen