Transaktionsmanagement mit mssql-python

Der mssql-python-Treiber unterstützt die vollständige Transaktionskontrolle, einschließlich Commit-, Rollback-, Autocommit-Konfiguration und Transaktionsisolation.

Transaktionsgrundlagen

Eine Transaktion gruppiert eine Abfolge von Datenbankoperationen zu einer einzigen Arbeitseinheit. Die Transaktionen folgen den ACID-Eigenschaften:

  • Atomität: Alle Operationen sind erfolgreich oder alle scheitern.
  • Konsistenz: Die Datenbank bleibt in einem gültigen Zustand.
  • Isolation: Gleichzeitige Transaktionen stören sich nicht gegenseitig.
  • Haltbarkeit: Engagierte Änderungen überstehen Systemausfälle.

Autocommit-Modus

Die Einstellung autocommit steuert, ob Änderungen automatisch ausgeführt werden.

Autocommit deaktiviert (Standard)

Standardmäßig ist dies autocommit=False. Du musst ausdrücklich Änderungen vornehmen.

import mssql_python

conn = mssql_python.connect(connection_string)
print(conn.autocommit)  # False

cursor = conn.cursor()
cursor.execute("CREATE TABLE #TxnBasic (Name NVARCHAR(50))")
cursor.execute("INSERT INTO #TxnBasic (Name) VALUES ('Widget')")
cursor.execute("INSERT INTO #TxnBasic (Name) VALUES ('Gadget')")

# Changes are staged but not visible to other connections
conn.commit()  # Now changes are permanent

conn.close()

Wenn du dich nicht bindest, verwirft der Treiber die Änderungen, wenn die Verbindung geschlossen wird.

Autocommit aktiviert

Wenn Sie autocommit=True festlegen, übernimmt der Treiber jede Anweisung sofort:

conn = mssql_python.connect(connection_string, autocommit=True)
# OR
conn.setautocommit(True)

cursor = conn.cursor()
cursor.execute("CREATE TABLE #AutoDemo (Name NVARCHAR(50))")
cursor.execute("INSERT INTO #AutoDemo (Name) VALUES ('Widget')")
# Immediately committed - no explicit commit needed

Caution

Wenn Autocommit aktiviert ist, kannst du mehrere Anweisungen nicht gemeinsam zurückrollen. Nutze Autocommit nur, wenn es für deinen Anwendungsfall geeignet ist.

Commit und Rollback

Commit

Rufen Sie commit() auf, um ausstehende Änderungen dauerhaft zu übernehmen:

cursor.execute("CREATE TABLE #CommitDemo (Name NVARCHAR(50), Price DECIMAL(10,2), CategoryID INT)")
cursor.execute("INSERT INTO #CommitDemo VALUES ('A',10,1),('B',20,1)")
cursor.execute("UPDATE #CommitDemo SET Price = Price * 1.1 WHERE CategoryID = 1")

conn.commit()  # The update is now permanent

Rollback

Um ausstehende Änderungen zu verwerfen, rufen Sie rollback() auf:

try:
    cursor.execute("CREATE TABLE #RollDemo (Name NVARCHAR(50), Price DECIMAL(10,2), CategoryID INT)")
    cursor.execute("INSERT INTO #RollDemo VALUES ('A',10,1),('B',20,1)")
    cursor.execute("UPDATE #RollDemo SET Price = Price * 1.1 WHERE CategoryID = 1")
    
    # Verify the update
    cursor.execute("SELECT AVG(Price) FROM #RollDemo WHERE CategoryID = 1")
    avg_price = cursor.fetchval()
    
    if avg_price > 100:
        conn.rollback()  # Price too high, undo both updates
        print("Rolled back: average price would exceed limit")
    else:
        conn.commit()
except Exception as e:
    conn.rollback()  # Undo on error
    raise

Commit und Rollback auf Cursor-Ebene

Der Einfachheit halber kannst du bei Cursorn commit() und rollback() aufrufen:

cursor = conn.cursor()
cursor.execute("CREATE TABLE #CursorDemo (Name NVARCHAR(50))")
cursor.execute("INSERT INTO #CursorDemo (Name) VALUES ('Widget')")
cursor.commit()  # Delegates to connection

cursor.execute("DELETE FROM #CursorDemo WHERE Name = 'Widget'")
cursor.rollback()  # Delegates to connection

Note

Commit und Rollback auf Cursorebene betreffen alle Cursor derselben Verbindung, nicht nur den Cursor, für den sie aufgerufen werden.

Kontextmanager

Der Verbindungs-Kontextmanager verbindet die Transaktion beim sauberen Abschluss und rollt sie zurück, wenn eine Ausnahme auftritt. Die Verbindung schließt sich immer beim Austreten. Wenn du setzt autocommit=True, haben Commit- und Rollback-Aufrufe keine Auswirkung.

with mssql_python.connect(connection_string) as conn:
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE #CtxDemo (Name NVARCHAR(50))")
    cursor.execute("INSERT INTO #CtxDemo (Name) VALUES ('Widget')")
    cursor.execute("INSERT INTO #CtxDemo (Name) VALUES ('Gadget')")
# Transaction is committed and connection is closed on exit

Tritt eine Ausnahme ein, wird die Transaktion zurückgesetzt:

try:
    with mssql_python.connect(connection_string) as conn:
        cursor = conn.cursor()
        cursor.execute("CREATE TABLE #TxnDemo (Name NVARCHAR(50))")
        cursor.execute("INSERT INTO #TxnDemo (Name) VALUES ('Widget')")
        raise ValueError("Something went wrong")
except ValueError:
    pass
# Transaction is rolled back and connection is closed on exit

Transaktionsisolationsstufen

Isolationsstufen steuern, wie Transaktionen mit gleichzeitigen Transaktionen interagieren. Stellen Sie das Isolationsniveau mit set_attr()ein:

import mssql_python

conn = mssql_python.connect(connection_string)

# Set isolation level
conn.set_attr(
    mssql_python.SQL_ATTR_TXN_ISOLATION,
    mssql_python.SQL_TXN_SERIALIZABLE
)

Verfügbare Isolationsstufen

Dauerhaft Beschreibung
SQL_TXN_READ_UNCOMMITTED Kann nicht kommittierte Änderungen von anderen Transaktionen lesen (Dirty Reads erlaubt).
SQL_TXN_READ_COMMITTED Liest nur bestätigte Daten (standardmäßig in SQL Server)
SQL_TXN_REPEATABLE_READ Garantiert konsistente Lesevorgänge innerhalb der Transaktion
SQL_TXN_SERIALIZABLE Höchste Isolation; Transaktionen scheinen fortlaufend zu laufen

Wählen Sie eine Isolationsstufe

Anwendungsfall Empfohlene Stufe
Allgemeine OLTP-Workloads READ_COMMITTED (Standardwert)
Berichte, die konsistente Schnappschüsse benötigen REPEATABLE_READ oder Momentaufnahme
Finanzielle Berechnungen, die Genauigkeit erfordern SERIALIZABLE
Leseintensive Arbeitslasten, die veraltete Daten tolerieren READ_UNCOMMITTED

Momentaufnahmeisolation

Zur Snapshot-Isolation verwenden Sie Transact-SQL (T-SQL). Snapshot-Isolation verwendet Zeilenversionierung in tempdb, was den Speicherbedarf bei hoher Schreiblast erhöhen kann.

# Enable snapshot isolation on the database (one-time setup, requires autocommit)
conn.commit()
conn.autocommit = True
cursor.execute("ALTER DATABASE AdventureWorks2022 SET ALLOW_SNAPSHOT_ISOLATION ON")

# Set isolation level while still in autocommit, then start the transaction
cursor.execute("SET TRANSACTION ISOLATION LEVEL SNAPSHOT")
conn.autocommit = False

cursor.execute("SELECT TOP 5 Name, ListPrice FROM Production.Product")
rows = cursor.fetchall()
for row in rows:
    print(row.Name, row.ListPrice)
conn.commit()

Verschachtelte Transaktionen und Speicherpunkte

SQL Server unterstützt Speicherpunkte für teilweise Rollbacks innerhalb einer Transaktion.

cursor = conn.cursor()

cursor.execute("BEGIN TRANSACTION")
cursor.execute("CREATE TABLE #SaveDemo (Name NVARCHAR(50))")
cursor.execute("INSERT INTO #SaveDemo (Name) VALUES ('Widget')")

cursor.execute("SAVE TRANSACTION SaveDemoPoint")
cursor.execute("INSERT INTO #SaveDemo (Name) VALUES ('Gadget')")

# Roll back to savepoint, keeping first insert
cursor.execute("ROLLBACK TRANSACTION SaveDemoPoint")

cursor.execute("COMMIT TRANSACTION")

Deadlocks beheben

Deadlocks treten auf, wenn zwei Transaktionen auf die Sperre der jeweils anderen warten. SQL Server erkennt automatisch Deadlocks und beendet eine Transaktion.

import time

def execute_with_retry(conn, cursor, sql, params=None, max_retries=3):
    """Execute SQL with deadlock retry logic."""
    for attempt in range(max_retries):
        try:
            cursor.execute(sql, params)
            return
        except mssql_python.OperationalError as e:
            if "1205" in str(e):  # Deadlock error number
                if attempt < max_retries - 1:
                    conn.rollback()  # Clear the failed transaction
                    time.sleep(0.1 * (2 ** attempt))  # Exponential backoff
                    continue
            raise
    raise Exception(f"Failed after {max_retries} attempts")

Bewährte Methoden

  • Halten Sie die Transaktionen kurz , um die Sperrdauer und das Deadlock-Potenzial zu minimieren.

  • Verwenden Sie autocommit=False für Mehraussagen-Transaktionen, die atomar sein sollten.

  • Behandle Ausnahmen immer mit Rollback:

    conn = None
    try:
         conn = mssql_python.connect(connection_string)
         cursor = conn.cursor()
         cursor.execute("CREATE TABLE #RollbackPattern (ID INT, Name NVARCHAR(50))")
         cursor.execute("INSERT INTO #RollbackPattern (ID, Name) VALUES (1, 'Widget')")
         cursor.execute("UPDATE #RollbackPattern SET Name = 'Updated Widget' WHERE ID = 1")
         conn.commit()
    except Exception:
         if conn is not None:
             conn.rollback()
         raise
    finally:
         if conn is not None:
             conn.close()
    
  • Nutze Kontextmanager, um Transaktionen automatisch zu verwalten. Der Kontextmanager führt bei fehlerfreiem Verlassen ein Commit durch und führt bei einer Ausnahme ein Rollback durch.

    with mssql_python.connect(connection_string) as conn:
         cursor = conn.cursor()
         cursor.execute("CREATE TABLE #ContextManagerDemo (ID INT, Name NVARCHAR(50))")
         cursor.execute("INSERT INTO #ContextManagerDemo (ID, Name) VALUES (1, 'Widget')")
         cursor.execute("UPDATE #ContextManagerDemo SET Name = 'Committed Widget' WHERE ID = 1")
    # Committed automatically on exit
    
  • Wähle passende Isolationsstufen basierend auf deinen Konsistenzanforderungen im Vergleich zu den Leistungsanforderungen.

  • Verwenden Sie Lock-Hinweise für Read-Modify-Write-Muster , um verlorene Updates zu verhindern. Wenn Sie einen Wert lesen, der innerhalb derselben Transaktion aktualisiert wird, verwenden Sie Hinweise wie WITH (UPDLOCK, ROWLOCK) bei SELECT, um Schlösser frühzeitig zu erhalten und eine konsistente Lock-Reihenfolge herzustellen, was das Deadlock-Risiko verringert.

    # Good: Acquire lock during read to prevent lost update pattern
    cursor.execute("""
         SELECT Balance FROM Accounts 
         WITH (UPDLOCK, ROWLOCK) 
         WHERE ID = %(id)s
    """, {"id": account_id})
    balance = cursor.fetchval()
    
    if balance >= amount:
         cursor.execute("""
             UPDATE Accounts SET Balance = Balance - %(amount)s 
             WHERE ID = %(id)s
         """, {"amount": amount, "id": account_id})
    
  • Implementiere Retry-Logik für vorübergehende Fehler wie Deadlocks.

Beispiel: Geld überweisen (atomare Operation)

Dieses Beispiel demonstriert die atomare Transferlogik mit Lock-Hinweisen, um verlorene Updates in gleichzeitigen Szenarien zu verhindern:

def transfer_funds(conn, from_account, to_account, amount):
    """Transfer funds atomically between accounts."""
    cursor = conn.cursor()
    
    try:
        # Read balance with lock hint to prevent lost updates
        cursor.execute(
            "SELECT Balance FROM Accounts WITH (UPDLOCK, ROWLOCK) WHERE AccountID = %(account_id)s",
            {"account_id": from_account}
        )
        balance = cursor.fetchval()
        
        if balance is None:
            raise ValueError(f"Source account {from_account} not found")
        
        if balance < amount:
            raise ValueError("Insufficient funds")
        
        # Debit source account
        cursor.execute(
            "UPDATE Accounts SET Balance = Balance - %(amount)s WHERE AccountID = %(account_id)s",
            {"amount": amount, "account_id": from_account}
        )
        
        # Credit destination account
        cursor.execute(
            "UPDATE Accounts SET Balance = Balance + %(amount)s WHERE AccountID = %(account_id)s",
            {"amount": amount, "account_id": to_account}
        )
        
        if cursor.rowcount != 1:
            raise ValueError(f"Destination account {to_account} not found")
        
        conn.commit()
        print(f"Transferred ${amount} from {from_account} to {to_account}")
        
    except Exception:
        conn.rollback()
        raise

Die Schlosshinweise WITH (UPDLOCK, ROWLOCK) auf der SELECT stellen sicher, dass das Schloss frühzeitig erlangt wird. Dies verhindert, dass eine andere Transaktion gleichzeitig denselben Saldo liest und ein Verloren-Update-Szenario erzeugt, bei dem beide Transaktionen den alten Saldo lesen, separate Updates durchführen und nur das letzte Update erhalten bleibt.

Temporäre Tabellenabgrenzung

Temporäre Tabellen (#tablename) sind auf die Sitzung abgegrenzt, aber ihre Erstellung ist Teil der aktuellen Transaktion. Wenn du eine temporäre Tabelle erstellst und die Transaktion zurückrollt, wird die temporäre Tabelle entfernt:

conn = mssql_python.connect(connection_string)  # autocommit=False
cursor = conn.cursor()

cursor.execute("CREATE TABLE #Staging (ID INT, Name NVARCHAR(50))")
cursor.execute("INSERT INTO #Staging VALUES (1, 'test')")

# Rollback removes the temp table entirely
conn.rollback()

# This fails: Invalid object name '#Staging'
try:
    cursor.execute("SELECT * FROM #Staging")
except mssql_python.ProgrammingError:
    print("Temp table was dropped by rollback")

Um eine temporäre Tabelle unabhängig von Ihrer Datentransaktion zu halten, führen Sie nach dem Erstellen ein Commit aus:

cursor.execute("CREATE TABLE #Staging (ID INT, Name NVARCHAR(50))")
conn.commit()  # Temp table persists regardless of later rollbacks

# Now data operations can roll back without losing the table
cursor.execute("INSERT INTO #Staging VALUES (1, 'test')")
conn.rollback()  # Data is gone, but #Staging still exists

DDL-Anweisungen, die Autocommit erfordern

Einige DDL-Anweisungen, wie CREATE DATABASE, ALTER DATABASE, und DROP DATABASE, können innerhalb einer Transaktion nicht ausgeführt werden. Setzen Sie autocommit=True vor der Ausführung fest:

conn.autocommit = True
cursor.execute("CREATE DATABASE TestDB")
conn.autocommit = False  # Return to transactional mode

Wenn Sie mit CREATE DATABASEausführenautocommit=False, erhalten Sie einen Fehler:CREATE DATABASE statement not allowed within multi-statement transaction.