web-dev-qa-db-fra.com

Transactions avec Python sqlite3

J'essaie de transférer du code vers Python qui utilise des bases de données SQLite, et j'essaie de faire en sorte que les transactions fonctionnent, et je suis vraiment dérouté. Je suis vraiment dérouté par cela. J'ai beaucoup utilisé sqlite dans d'autres langues, parce que c'est génial, mais je ne peux tout simplement pas comprendre ce qui ne va pas ici.

Voici le schéma de ma base de données de test (à insérer dans l'outil de ligne de commande sqlite3).

BEGIN TRANSACTION;
CREATE TABLE test (i integer);
INSERT INTO "test" VALUES(99);
COMMIT;

Voici un programme de test.

import sqlite3

sql = sqlite3.connect("test.db")
with sql:
    c = sql.cursor()
    c.executescript("""
        update test set i = 1;
        fnord;
        update test set i = 0;
        """)

Vous remarquerez peut-être l'erreur délibérée. Cela provoque l'échec du script SQL sur la deuxième ligne après l'exécution de la mise à jour.

Selon la documentation, l'instruction with sql est supposée configurer une transaction implicite autour du contenu, qui n'est validée que si le blocage aboutit. Cependant, lorsque je l'exécute, l'erreur SQL attendue ... mais la valeur de i est définie entre 99 et 1. Je m'attends à ce qu'elle reste à 99, car cette première mise à jour doit être annulée.

Voici un autre programme de test, qui appelle explicitement commit() et rollback().

import sqlite3

sql = sqlite3.connect("test.db")
try:
    c = sql.cursor()
    c.executescript("""
        update test set i = 1;
        fnord;
        update test set i = 0;
    """)
    sql.commit()
except sql.Error:
    print("failed!")
    sql.rollback()

Cela se comporte exactement de la même manière --- je passe de 99 à 1.

Maintenant, j'appelle BEGIN et COMMIT explicitement:

import sqlite3

sql = sqlite3.connect("test.db")
try:
    c = sql.cursor()
    c.execute("begin")
    c.executescript("""
            update test set i = 1;
            fnord;
            update test set i = 0;
    """)
    c.execute("commit")
except sql.Error:
    print("failed!")
    c.execute("rollback")

Cela échoue aussi, mais d'une manière différente. J'ai compris:

sqlite3.OperationalError: cannot rollback - no transaction is active

Cependant, si je remplace les appels de c.execute() à c.executescript(), alors il fonctionne (je reste à 99)!

(Je devrais aussi ajouter que si je mets les begin et commit dans l'appel interne à executescript, alors elle se comporte correctement dans tous les cas, mais malheureusement, je ne peux pas utiliser cette approche dans mon application. De plus, changer sql.isolation_level ne semble faire aucune différence. au comportement.)

Quelqu'un peut-il m'expliquer ce qui se passe ici? J'ai besoin de comprendre ceci; si je ne peux pas faire confiance aux transactions dans la base de données, je ne peux pas faire fonctionner mon application ...

Python 2.7, python-sqlite3 2.6.0, sqlite3 3.7.13, Debian.

28
David Given

L'API de base de données Python tente d'être intelligente et commence et valide automatiquement les transactions .

Je recommanderais d'utiliser un pilote de base de données qui not n'utilise pas l'API de base de données Python, telle que apsw .

12
CL.

Pour tous ceux qui souhaitent utiliser sqlite3 lib quelles que soient ses lacunes, j'ai constaté que vous pouvez garder un certain contrôle sur les transactions si vous effectuez ces deux opérations:

  1. set Connection.isolation_level = None (comme dans docs , cela signifie le mode autocommit)
  2. évitez d'utiliser executescript du tout, car selon le docs , il "émet d'abord une instruction COMMIT", c'est-à-dire un problème En effet, j'ai trouvé qu'il interfère avec toutes les transactions définies manuellement

Ainsi, l'adaptation suivante de votre test fonctionne pour moi:

import sqlite3

sql = sqlite3.connect("/tmp/test.db")
sql.isolation_level = None
try:
    c = sql.cursor()
    c.execute("begin")
    c.execute("update test set i = 1")
    c.execute("fnord")
    c.execute("update test set i = 0")
    c.execute("commit")
except sql.Error:
    print("failed!")
    c.execute("rollback")
25
yungchin

Per les docs ,

Les objets de connexion peuvent être utilisés en tant que gestionnaires de contexte qui automatiquement commettre ou annuler des transactions. En cas d'exception, le fichier la transaction est annulée; sinon, la transaction est validée:

Par conséquent, si vous laissez Python quitter l'instruction with avec lorsqu'une exception se produit, la transaction sera annulée.

import sqlite3

filename = '/tmp/test.db'
with sqlite3.connect(filename) as conn:
    cursor = conn.cursor()
    sqls = [
        'DROP TABLE IF EXISTS test',
        'CREATE TABLE test (i integer)',
        'INSERT INTO "test" VALUES(99)',]
    for sql in sqls:
        cursor.execute(sql)
try:
    with sqlite3.connect(filename) as conn:
        cursor = conn.cursor()
        sqls = [
            'update test set i = 1',
            'fnord',   # <-- trigger error
            'update test set i = 0',]
        for sql in sqls:
            cursor.execute(sql)
except sqlite3.OperationalError as err:
    print(err)
    # near "fnord": syntax error
with sqlite3.connect(filename) as conn:
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM test')
    for row in cursor:
        print(row)
        # (99,)

les rendements

(99,)

comme prévu.

17
unutbu

Voici ce que je pense se passe d'après ma lecture des liaisons sqlite3 de Python ainsi que de la documentation officielle de Sqlite3. La réponse courte est que si vous voulez une transaction correcte, vous devriez vous en tenir à cet idiome:

with connection:
    db.execute("BEGIN")
    # do other things, but do NOT use 'executescript'

Contrairement à mon intuition, with connection appelle pas et appelle BEGIN lors de la saisie de la portée. En fait, il ne fait rien du tout dans __enter__ . Cela n'a d'effet que lorsque vous __exit__ l'étendue, en choisissant COMMIT ou ROLLBACK, selon que l'étendue se ferme normalement ou avec une exception .

Par conséquent, la bonne chose à faire est de toujours marquer explicitement le début de vos transactions en utilisant BEGIN. Cela rend isolation_levelirrelevant dans les transactions, car heureusement, il n'a d'effet que si le mode autocommit est activé et le mode autocommit est toujours supprimé dans les blocs de transaction .

Un autre problème est executescript, qui émet toujours un COMMIT avant d'exécuter votre script . Cela peut facilement gâcher les transactions, vous avez donc le choix entre:

  • utilisez exactement un executescript dans une transaction et rien d'autre, ou
  • éviter executescript entièrement; vous pouvez appeler execute autant de fois que vous le souhaitez, sous réserve de la limitation d'une instruction-par -execute.
4
Rufflewind

Les fonctions normales de .execute() fonctionnent normalement avec le mode de validation automatique par défaut très confortable et le gestionnaire de contexte with conn: ... effectuant la restauration automatique OU - à l'exception des transactions protégées lecture/modification/écriture, expliquées à la fin de cette réponse.

conn_or_cursor.executescript() non standard du module sqlite3 ne participe pas au mode de validation automatique (par défaut) (et ne fonctionne donc pas normalement avec le gestionnaire de contexte with conn: ...), mais transmet le script plutôt brut. Par conséquent, il ne fait que valider une transaction potentiellement en attente à validation automatique au début, avant de "passer à l'état brut".

Cela signifie également que sans "BEGIN" dans le script executescript() fonctionne sans transaction, et donc sans option d'annulation en cas d'erreur ou autre.

Donc, avec executescript(), nous ferions mieux d'utiliser un BEGIN explicite (tout comme votre script de création de schéma initial l'avait fait pour l'outil de ligne de commande sqlite en mode "brut"). Et cette interaction montre étape par étape ce qui se passe:

>>> list(conn.execute('SELECT * FROM test'))
[(99,)]
>>> conn.executescript("BEGIN; UPDATE TEST SET i = 1; FNORD; COMMIT""")
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
OperationalError: near "FNORD": syntax error
>>> list(conn.execute('SELECT * FROM test'))
[(1,)]
>>> conn.rollback()
>>> list(conn.execute('SELECT * FROM test'))
[(99,)]
>>> 

Le script n'a pas atteint le "COMMIT". Et ainsi nous pourrions voir l’état intermédiaire actuel et décider d’une annulation (ou s’engager quand même)

Ainsi, un try-except-rollback actif via excecutescript() ressemble à ceci:

>>> list(conn.execute('SELECT * FROM test'))
[(99,)]
>>> try: conn.executescript("BEGIN; UPDATE TEST SET i = 1; FNORD; COMMIT""")
... except Exception as ev: 
...     print("Error in executescript (%s). Rolling back" % ev)
...     conn.executescript('ROLLBACK')
... 
Error in executescript (near "FNORD": syntax error). Rolling back
<sqlite3.Cursor object at 0x011F56E0>
>>> list(conn.execute('SELECT * FROM test'))
[(99,)]
>>> 

(Notez la restauration via le script ici, car aucune .execute() n'a repris le contrôle de validation)


Et voici une note sur le mode auto-commit en combinaison avec le problème plus difficile d’une transaction protected read-modify-write - qui a fait dire à @Jeremie "De toutes les nombreuses choses écrites sur les transactions dans sqlite/python, c’est la seule chose qui me permet de faire ce que je veux (avoir un verrou de lecture exclusif sur la base de données). "dans un commentaire sur un exemple qui incluait un c.execute("begin"). Bien que sqlite3 ne crée normalement pas un verrou de lecture exclusif avec blocage long, sauf pour la durée de l'écriture en retour, mais des verrous à 5 étapes plus astucieux pour assurer une protection suffisante contre les modifications qui se chevauchent.

Le contexte de validation automatique with conn: ne place pas déjà ou ne déclenche pas un verrou suffisamment fort pour une lecture-modification-écriture protégée dans le schéma de verrouillage à 5 étapes de sqlite3 . Un tel verrou n'est implicitement créé que lorsque la première commande de modification de données est émise - donc trop tard . Seul un BEGIN (DEFERRED) (TRANSACTION) explicite déclenche le comportement souhaité: 

La première lecture Une opération sur une base de données crée un verrou SHARED et la première opération d'écriture crée un verrou RÉSERVÉ.

Ainsi, une transaction protégée lecture-modification-écriture qui utilise le langage de programmation de manière générale (et non une clause atomique SQL UPDATE spéciale) ressemble à ceci:

with conn:
    conn.execute('BEGIN TRANSACTION')    # crucial !
    v = conn.execute('SELECT * FROM test').fetchone()[0]
    v = v + 1
    time.sleep(3)  # no read lock in effect, but only one concurrent modify succeeds
    conn.execute('UPDATE test SET i=?', (v,))

En cas d'échec, cette transaction lecture-modification-écriture peut être réessayée plusieurs fois.

2
kxr

Vous pouvez utiliser la connexion en tant que gestionnaire de contexte. Il annulera alors automatiquement les transactions en cas d'exception ou les validera autrement.

try:
    with con:
        con.execute("insert into person(firstname) values (?)", ("Joe",))

except sqlite3.IntegrityError:
    print("couldn't add Joe twice")

Voir https://docs.python.org/3/library/sqlite3.html#using-the-connection-as-a-context-manager

2
Gabriel Cypriano

C'est un peu vieux fil mais si cela aide, j'ai trouvé que faire une restauration sur l'objet de connexion fait l'affaire.

0
Saliya Ekanayake