Транзакции с 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()

Это ведет себя точно так же --- i меняется с 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() на c.executescript(), тогда он работает (я остаюсь на 99)!

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

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

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


person David Given    schedule 06.04.2013    source источник


Ответы (7)


Python DB API старается быть умным и начинает и фиксирует транзакции автоматически. .

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

person CL.    schedule 07.04.2013
comment
Спасибо, apsw - это именно то, что я ищу. Я все еще в замешательстве; если обработка транзакций python-sqlite3 нарушена, почему никто не заметил и не исправил это, учитывая, что это привязка Sqlite по умолчанию для Python? Конечно, транзакции являются основной компетенцией любой библиотеки SQL? - person David Given; 09.04.2013
comment
Неисправен API Python; и его нельзя легко изменить по соображениям обратной совместимости. - person CL.; 09.04.2013
comment
Что ж, мой вопрос остается в силе: почему Python API делает это? - person David Given; 10.04.2013
comment
Так решает ли эта проблема установка isolation_level = None в библиотеке sqlite3 (согласно ответу @yungchin)? Или все же лучше использовать APSW? (python 3.5, если это имеет значение.) - person max; 12.03.2016
comment
@max Это зависит от того, какой API вы предпочитаете. - person CL.; 12.03.2016
comment
Ну, проблема в другом: executescript() работает на самом деле необработанным образом, поэтому вам нужно BEGIN ... COMMIT внутри скрипта, как обычно в других местах, для явной опции ROLLBACK в except. С умными .execute() и прочими функциями и with conn: обычно тоже проблем нет - это удобнее, безопаснее и последовательнее. Просто для защищенной транзакции чтения-изменения-записи необходимо явное НАЧАЛО. Подробности смотрите в другом моем посте. isolation_level = None уже используется по умолчанию. - person kxr; 18.05.2017
comment
Что касается python3.6, документы гласят: Изменено в версии 3.6: sqlite3 используется для неявной фиксации открытой транзакции перед операторами DDL. Это больше не так. - person Jacob Lee; 05.10.2018

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

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

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

import sqlite3

sql = sqlite3.connect("/tmp/test.db")
sql.isolation_level = None
c = sql.cursor()
c.execute("begin")
try:
    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")
person yungchin    schedule 13.05.2014
comment
Думаю так и должно быть, так как на фнорде провалишь и потом запустишь откат. - person rsaxvc; 26.01.2015
comment
Спасибо. Из всего много-много написанного о транзакциях в sqlite/python, это единственное, что позволяет мне делать то, что я хочу (иметь эксклюзивную блокировку чтения в базе данных). - person Jérémie; 21.09.2015
comment
Что означает режим автоматической фиксации? Судя по его названию, я предполагаю, что любой оператор MDL будет зафиксирован автоматически. Но судя по вашему объяснению, моя догадка неверна. Кто мог бы мне помочь? И я искал его в Google, также не мог получить никакого полезного ответа. - person Clock ZHONG; 16.03.2017
comment
↑ Я могу ответить на часть автокоммита. isolation_level = None отключает автоматическую обработку Python-оболочкой выдачи BEGIN и т. д. для вас. То, что осталось, — это базовая библиотека C, которая по умолчанию выполняет автоматическую фиксацию. Однако эта автоматическая фиксация отключается, когда вы выполняете BEGIN (потому что вы сигнализируете транзакцию с помощью этого оператора), поэтому приведенное выше работает именно так. Документация по SQLite - person Thanatos; 06.02.2019
comment
Я поиграл с двумя строками, потому что, если они выкинут, вы не захотите делать откат, но полезный ответ! - person UKMonkey; 04.05.2019
comment
Я добавлю, что вы также можете установить уровень транзакции. Например c.execute("begin exclusive transaction"). - person Marius; 12.02.2020
comment
Я запутался. В чем разница между c.execute("commit") и sql.commit()? - person HelloGoodbye; 15.10.2020
comment
@HelloGoodbye может и не быть разницы (я никогда не проверял, но, судя по документации, разницы быть не должно). Я думаю, что, вероятно, отчасти я просто пытался держаться ближе к последнему листингу в вопросе, а отчасти я также предпочитаю это эстетически, потому что закрытие транзакции, которая открылась с c.execute("begin") с sql.commit(), выглядит немного сломанным (даже если это не так) . - person yungchin; 16.10.2020
comment
В этом есть смысл. Итак, есть ли метод также в sql, который соответствует c.execute("begin")? Или только c.execute("commit") имеет соответствующий метод в sql? - person HelloGoodbye; 18.10.2020
comment
@HelloGoodbye Если транзакция выполняется, то c.commit() и c.execute("commit") имеют одинаковый эффект. Разница только в том, что транзакция не выполняется; в этом случае c.commit() молча ничего не делает, а c.execute("commit") выдает исключение. c.rollback() похоже. Нет эквивалента для начала транзакции. - person Jim Oldfield; 18.12.2020
comment
Ваш код правильный, но объяснение сбивает с толку. Вы говорите, что isolation_level = None переводит базу данных в режим автоматической фиксации, но это неверно или, по крайней мере, вводит в заблуждение: база данных всегда находится в режиме автоматической фиксации, независимо от isolation_level, пока транзакция в данный момент не открыта. Оператор можно выполнить вне транзакции, то есть в режиме автоматической фиксации, даже если isolation_level не является None. - person Jim Oldfield; 23.12.2020

Согласно документам,

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

Поэтому, если вы позволите 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,)

как и ожидалось.

person unutbu    schedule 01.11.2014
comment
+1, это работает, за исключением того, что все, что выполняется executescript, не будет откатываться, даже если оно находится в блоке 'with-statement', потому что executescript сначала выдает COMMIT. - person MLister; 07.05.2015
comment
-1 Открытие нового соединения для каждой транзакции крайне неэффективно и совершенно не нужно. Вы также не сказали, когда начинается транзакция (например, знаете ли вы, что ваш первый блок, который удаляет и создает таблицу, на самом деле состоит из трех отдельных транзакций?) - person Jim Oldfield; 23.12.2020

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

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

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

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

Еще одна особенность — executescript, которая всегда выдает COMMIT перед запуском скрипта. . Это может легко испортить транзакционный блок with connection, поэтому вы можете либо

  • использовать только один executescript в блоке with и ничего больше, или
  • полностью избегать executescript; вы можете вызывать execute столько раз, сколько хотите, с учетом ограничения на один оператор на execute.
person Rufflewind    schedule 09.06.2017
comment
использовать только один executescript внутри транзакции. Это неправильно, потому что, как вы уже сказали, executescript выдает коммит заранее, поэтому поместить его в транзакцию невозможно. Я думаю, вы хотели сказать обратное: поместить транзакцию внутри executescript. - person Jim Oldfield; 23.12.2020
comment
Я думаю, когда я писал транзакцию, я действительно имел в виду один блок with connection:. - person Rufflewind; 24.12.2020
comment
Хорошее исследование, чтобы получить краткое поведение: поэтому правильно всегда явно отмечать начало вашей транзакции блоками подключения, используя BEGIN. - person Rfraile; 19.01.2021

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

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

Это также означает, что без «НАЧАЛА» внутри скрипта executescript() работает без транзакции и, следовательно, без возможности отката в случае ошибки или иным образом.

Поэтому с executescript() нам лучше использовать явное НАЧАЛО (так же, как ваш начальный сценарий создания схемы делал для инструмента командной строки 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,)]
>>> 

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

Таким образом, рабочий try-except-rollback через 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. Такая блокировка выполняется неявно только тогда, когда выдается первая команда модификации данных - поэтому слишком поздно. Только явный BEGIN (DEFERRED) (TRANSACTION) вызывает желаемое поведение:

Первая операция чтения базы данных создает блокировку SHARED, а первая операция записи создает блокировку RESERVED. .

Таким образом, защищенная транзакция чтения-модификации-записи, использующая язык программирования в общем виде (а не специальное предложение атомарного 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,))

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

person kxr    schedule 18.05.2017
comment
-1 Вы используете режим автоматической фиксации фразы, чтобы обозначить практику модуля Python sqlite3 для автоматического запуска транзакций. Но эта фраза означает что-то очень конкретное и совсем другое (почти противоположное!): сама базовая библиотека sqlite автоматически поместит мини-транзакцию вокруг каждого отдельного оператора, а не в транзакции. Таким образом, всякий раз, когда модуль Python автоматически запускает транзакцию, он фактически выводит базу данных выводит из режима автофиксации! Но вы правы в том, что транзакции не запускаются автоматически операторами SELECT. - person Jim Oldfield; 23.12.2020

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

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

person Gabriel Saca    schedule 16.01.2017
comment
Вопрос был в основном о том, когда начинаются транзакции. На самом деле вопрос, на который вы отвечаете, уже использует соединение в качестве менеджера контекста в их первом примере. - person Jim Oldfield; 23.12.2020

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

person Saliya Ekanayake    schedule 08.04.2018