Transactions with Python sqlite3

后端 未结 7 1640
梦如初夏
梦如初夏 2020-12-07 17:33

I\'m trying to port some code to Python that uses sqlite databases, and I\'m trying to get transactions to work, and I\'m getting really confused. I\'m really confused by th

7条回答
  •  有刺的猬
    2020-12-07 18:16

    Normal .execute()'s work as expected with the comfortable default auto-commit mode and the with conn: ... context manager doing auto-commit OR rollback - except for protected read-modify-write transactions, which are explained at the end of this answer.

    sqlite3 module's non-standard conn_or_cursor.executescript() doesn't take part in the (default) auto-commit mode (and so doesn't work normally with the with conn: ... context manager) but forwards the script rather raw. Therefor it just commits a potentially pending auto-commit transactions at start, before "going raw".

    This also means that without a "BEGIN" inside the script executescript() works without a transaction, and thus no rollback option upon error or otherwise.

    So with executescript() we better use a explicit BEGIN (just as your inital schema creation script did for the "raw" mode sqlite command line tool). And this interaction shows step by step whats going on:

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

    The script didn't reach the "COMMIT". And thus we could the view the current intermediate state and decide for rollback (or commit nevertheless)

    Thus a working try-except-rollback via excecutescript() looks like this:

    >>> 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
    
    >>> list(conn.execute('SELECT * FROM test'))
    [(99,)]
    >>> 
    

    (Note the rollback via script here, because no .execute() took over commit control)


    And here a note on the auto-commit mode in combination with the more difficult issue of a protected read-modify-write transaction - which made @Jeremie say "Out of all the many, many things written about transactions in sqlite/python, this is the only thing that let me do what I want (have an exclusive read lock on the database)." in a comment on an example which included a c.execute("begin"). Though sqlite3 normally does not make a long blocking exclusive read lock except for the duration of the actual write-back, but more clever 5-stage locks to achieve enough protection against overlapping changes.

    The with conn: auto-commit context does not already put or trigger a lock strong enough for protected read-modify-write in the 5-stage locking scheme of sqlite3. Such lock is made implicitely only when the first data-modifying command is issued - thus too late. Only an explicit BEGIN (DEFERRED) (TRANSACTION) triggers the wanted behavior:

    The first read operation against a database creates a SHARED lock and the first write operation creates a RESERVED lock.

    So a protected read-modify-write transaction which uses the programming language in general way (and not a special atomic SQL UPDATE clause) looks like this:

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

    Upon failure such read-modify-write transaction could be retried a couple of times.

提交回复
热议问题