用作上下文管理器的 sqlite3 连接的事务不是原子的

P-G*_*-Gn 1 python sqlite

根据文档

连接对象可用作自动提交或回滚事务的上下文管理器。发生异常时,事务回滚;否则,事务被提交:

我知道with语句中的所有内容都应该是原子事务。现在考虑这个代码

import sqlite3
con = sqlite3.connect(':memory:')

try:
  with con:
    con.execute('create table foo (id integer primary key)')
    con.execute('insert into foo values (1)')
    con.execute('insert into foo values (1)')
except sqlite3.Error:
  print('transaction failed')

try:
  rec = con.execute('select count(*) from foo')
  print('number of records: {}'.format(rec.fetchone()[0]))
except sqlite3.Error as e:
  print(e)
Run Code Online (Sandbox Code Playgroud)

返回

import sqlite3
con = sqlite3.connect(':memory:')

try:
  with con:
    con.execute('create table foo (id integer primary key)')
    con.execute('insert into foo values (1)')
    con.execute('insert into foo values (1)')
except sqlite3.Error:
  print('transaction failed')

try:
  rec = con.execute('select count(*) from foo')
  print('number of records: {}'.format(rec.fetchone()[0]))
except sqlite3.Error as e:
  print(e)
Run Code Online (Sandbox Code Playgroud)

一方面,由于重复值,交易失败。另一方面,表foo存在,即使它是空的,这意味着第一个插入已回滚。表创建不应该也回滚吗?

“手动”进行交易会产生预期的结果:

import sqlite3
con = sqlite3.connect(':memory:')

con.execute('begin')
try:
  con.execute('create table foo (id integer primary key)')
  con.execute('insert into foo values (1)')
  con.execute('insert into foo values (1)')
  con.execute('commit')
except sqlite3.Error:
  con.execute('rollback')
  print('transaction failed')

try:
  rec = con.execute('select count(*) from foo')
  print('number of records: {}'.format(rec.fetchone()[0]))
except sqlite3.Error as e:
  print(e)
Run Code Online (Sandbox Code Playgroud)

返回

transaction failed
number of records: 0
Run Code Online (Sandbox Code Playgroud)

为什么会出现差异?

Mar*_*ers 5

从 Python 3.6 开始,DDL 或数据定义语言语句(如CREATE TABLE)不会启动事务。这意味着任何此类语句都会在您执行时自动提交。

请参阅控制交易部分

默认情况下,sqlite3模块打开交易数据修改语言(DML)语句之前隐式(即INSERT/ UPDATE/ DELETE/ REPLACE)。

[...]

在 3.6 版更改sqlite3用于在 DDL 语句之前隐式提交打开的事务。这已不再是这种情况。

这意味着如果您希望 DDL 语句成为事务的一部分,则必须显式启动事务。

使用连接作为上下文管理器仍然只在退出时发出提交回滚,它不会启动事务;相反,遇到的第一个 DML 语句将启动一个。如果您希望 DDL 成为事务的一部分,请begin在顶部添加一条语句:

try:
    with con:
        con.execute('begin')  # explicit, rather than implicit, transaction start
        con.execute('create table foo (id integer primary key)')
        con.execute('insert into foo values (1)')
        con.execute('insert into foo values (1)')
except sqlite3.Error:
    print('transaction failed')
Run Code Online (Sandbox Code Playgroud)