Транзакции с Python sqlite3

я пытаюсь перенести некоторый код на Python, который использует базы данных sqlite, и я пытаюсь заставить транзакции работать, и я действительно запутался. Я действительно смущен этим; я много использовал sqlite на других языках, потому что это здорово, но я просто не могу понять, что здесь не так.

вот схема для моей тестовой базы данных (для ввода в средство командной строки sqlite3).

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

вот тестовая программа.

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;
        """)

вы можете обратите внимание на преднамеренную ошибку. Это приводит к сбою сценария SQL во второй строке после выполнения обновления.

согласно документам,with sql оператор должен настроить неявную транзакцию вокруг содержимого, которая фиксируется только в случае успеха блока. Однако, когда я запускаю его, я получаю ожидаемую ошибку SQL... но значение i установлено от 99 до 1. Я ожидаю, что он останется на 99, потому что это первое обновление должно быть свернуто спина.

вот еще одна тестовая программа, которая прямо называет commit() и 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()

это ведет себя точно так же --- я меняется с 99 на 1.

теперь я вызываю BEGIN и COMMIT явно:

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")

и это не удается, но по-другому. Я понимаю:

sqlite3.OperationalError: cannot rollback - no transaction is active

однако, если я заменю вызовы c.execute() to c.executescript(), потом работает (я по-прежнему в 99)!

(я также должен добавить, что если я поставлю begin и commit внутри внутреннего вызова executescript потом он ведет себя правильно во всех случаях, но, к сожалению, я не могу использовать этот подход в своем приложении. Кроме того, изменение sql.isolation_level кажется, не имеет никакого значения для поведения.)

может кто-нибудь объяснит мне, что здесь происходит? Мне нужно это понять; если я не могу доверять транзакциям в базе данных, я не могу сделать свое приложение работа...

в Python 2.7, в Python-sqlite3 для 2.6.0, и sqlite3, 3.7.13, Дебиан.

7 ответов


API БД Python пытается быть умным, и начинает и совершает сделки автоматически.

Я бы рекомендовал использовать драйвер DB, который делает не используйте API БД Python, например apsw.


для всех, кто хотел бы работать с sqlite3 lib независимо от его недостатков, я обнаружил, что вы можете сохранить некоторый контроль над транзакциями, если вы сделаете эти две вещи:

  1. set Connection.isolation_level = None (согласно docs, это означает автоматический режим)
  2. избегайте использования executescript вообще, потому что согласно docs он "сначала выдает заявление о фиксации" - т. е. проблема. Действительно, я обнаружил, что это мешает любому ручному набору сделок

Итак, следующая адаптация вашего теста работает для меня:

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")

Per документы,

объекты соединения можно использовать как менеджеры контекста которые автоматически фиксация или откат транзакций. В случае исключения транзакция откатывается; в противном случае транзакция фиксируется:

поэтому, если вы позволите Python выйти из оператора with при возникновении исключения, транзакция будет откатываться.

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,)

доходность

(99,)

как ожидаемый.


вот что я думаю, что происходит на основе моего чтения Привязок sqlite3 Python, а также официальных документов Sqlite3. Короткий ответ заключается в том, что если вы хотите правильную транзакцию, вы должны придерживаться этой идиомы:

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

вопреки моей интуиции,with connection тут не вызов BEGIN при входе в сферу. На самом деле это вообще ничего не делает в __enter__. Это имеет эффект только тогда, когда вы __exit__ область, выбор либо COMMIT или ROLLBACK в зависимости от того, выходит ли область обычно или за исключением.

поэтому правильная вещь - всегда явно отмечать начало ваших транзакций с помощью BEGIN. Это делает isolation_level значения в транзакциях, потому что, к счастью, это имеет эффект только в то время как режим autocommit включен и режим autocommit всегда подавляется в транзакции блоки.

еще одна причуда executescript, который всегда COMMIT перед запуском скрипта. Это может легко испортить транзакции, поэтому ваш выбор-либо

  • использовать только один executescript в рамках транзакции и больше ничего, или
  • избежать executescript полностью; вы можете позвонить execute столько раз, сколько вы хотите, при условии, что one-statement-per -execute ограничения.

вы можете использовать соединение в качестве контекстного менеджера. Затем он автоматически откатывает транзакции в случае исключения или фиксирует их иным образом.

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

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

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


нормальный .execute()работает, как и ожидалось, с удобным режимом автоматической фиксации по умолчанию и with conn: ... context manager делает автоматическую фиксацию или откат - кроме защищенные транзакции чтения-изменения-записи, который пояснил в конце этого ответа.

нестандартный модуль sqlite3 conn_or_cursor.executescript() не участвует в режиме автоматической фиксации (по умолчанию) (и поэтому не работает нормально с with conn: ... context manager), но перенаправляет скрипт скорее необработанный. Поэтому он просто совершает потенциально до автоматической фиксации транзакции в начало до "сыроедения".

это также означает, что без "начать" внутри скрипта executescript() работает без транзакции, и, следовательно, нет опции отката при ошибке или в противном случае.

поэтому с executescript() мы лучше используем явное начало (так же, как ваш сценарий создания схемы inital сделал для инструмента командной строки "raw" SQLite). И это взаимодействие показывает шаг за шагом, что происходит:

>>> 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,)]
>>> 

сценарий не достиг "фиксации". И таким образом, мы могли бы просмотреть текущее промежуточное состояние и решить для отката (или, тем не менее, совершить)

таким образом, рабочая попытка-кроме-отката через excecutescript() выглядит так:

>>> 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,)]
>>> 

(обратите внимание на откат через скрипт здесь, потому что нет .execute() взял на себя контроль фиксации)


и здесь примечание о режиме автоматической фиксации в сочетании с более трудный вопрос защищенная транзакция чтения-изменения-записи - что заставило @Jeremie сказать"из всех многих, многих вещей, написанных о транзакциях в sqlite / python, это единственное, что позволяет мне делать то, что я хочу (иметь эксклюзивную блокировку чтения в базе данных). " в комментарии к примеру, который включал c.execute("begin"). Хотя sqlite3 обычно не делает длинную блокировку эксклюзивной блокировки чтения, за исключением продолжительности фактической обратной записи, но более умной 5-ступенчатые замки для достижения достаточной защиты от перекрывающихся изменений.

на with conn: контекст автоматической фиксации уже не помещает или не запускает блокировку, достаточно сильную для защищенного чтения-изменения-записи в 5-ступенчатая схема блокировки sqlite3. Такой замок выполнен implicitely только тогда, когда первые данные-изменение команды - так поздно. Только явное BEGIN (DEFERRED) (TRANSACTION) запускает желаемое поведение:

первое чтение операция против базы данных создает общую блокировку и первая операция записи создает зарезервированную блокировку.

таким образом, защищенная транзакция чтения-изменения-записи, которая использует язык программирования в общем виде (а не специальное предложение atomic SQL UPDATE) выглядит следующим образом:

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,))

при сбое такая транзакция чтения-изменения-записи может быть повторена несколько раз.


Это немного старый поток, но если это помогает, я обнаружил, что выполнение отката на объекте соединения делает трюк.