Bemærk
Adgang til denne side kræver godkendelse. Du kan prøve at logge på eller ændre mapper.
Adgang til denne side kræver godkendelse. Du kan prøve at ændre mapper.
The mssql-python driver supports full transaction control, including commit, rollback, autocommit configuration, and transaction isolation levels.
Transaction basics
A transaction groups a sequence of database operations into a single unit of work. Transactions follow the ACID properties:
- Atomicity: All operations succeed or all fail.
- Consistency: Database remains in a valid state.
- Isolation: Concurrent transactions don't interfere with each other.
- Durability: Committed changes survive system failures.
Autocommit mode
The autocommit setting controls whether changes are committed automatically.
Autocommit disabled (default)
By default, autocommit=False. You must explicitly commit changes.
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()
If you don't commit, the driver discards changes when the connection closes.
Autocommit enabled
When you set autocommit=True, the driver commits each statement immediately:
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
When autocommit is enabled, you can't roll back multiple statements as a group. Use autocommit only when it's appropriate for your use case.
Commit and rollback
Commit
Call commit() to make pending changes permanent:
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
To discard pending changes, call rollback():
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
Cursor-level commit and rollback
For convenience, you can call commit() and rollback() on cursors:
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
Cursor-level commit and rollback affect all cursors on the same connection, not just the cursor on which you call it.
Context managers
The connection context manager commits the transaction on clean exit and rolls it back if an exception occurs. The connection always closes on exit. When you set autocommit=True, the commit and rollback calls have no effect.
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
If an exception occurs, the transaction is rolled back:
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
Transaction isolation levels
Isolation levels control how transactions interact with concurrent transactions. Set the isolation level using set_attr():
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
)
Available isolation levels
| Constant | Description |
|---|---|
SQL_TXN_READ_UNCOMMITTED |
Can read uncommitted changes from other transactions (dirty reads allowed) |
SQL_TXN_READ_COMMITTED |
Only reads committed data (default in SQL Server) |
SQL_TXN_REPEATABLE_READ |
Guarantees consistent reads within transaction |
SQL_TXN_SERIALIZABLE |
Highest isolation; transactions appear to run sequentially |
Choose an isolation level
| Use case | Recommended level |
|---|---|
| General OLTP workloads | READ_COMMITTED (default) |
| Reports that need consistent snapshots | REPEATABLE_READ or snapshot |
| Financial calculations requiring accuracy | SERIALIZABLE |
| Read-heavy workloads tolerating stale data | READ_UNCOMMITTED |
Snapshot isolation
For snapshot isolation, use Transact-SQL (T-SQL). Snapshot isolation uses row versioning in tempdb, which can increase storage requirements under heavy write workloads.
# 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()
Nested transactions and savepoints
SQL Server supports savepoints for partial rollback within a transaction.
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")
Handle deadlocks
Deadlocks occur when two transactions wait for each other's locks. SQL Server automatically detects deadlocks and terminates one transaction.
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")
Best practices
Keep transactions short to minimize lock duration and deadlock potential.
Use autocommit=False for multi-statement transactions that should be atomic.
Always handle exceptions with 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()Use context managers to manage transactions automatically. The context manager commits on clean exit and rolls back on exception.
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 exitChoose appropriate isolation levels based on your consistency requirements vs. performance needs.
Use lock hints for read-modify-write patterns to prevent lost updates. When you read a value that will be updated within the same transaction, use hints like
WITH (UPDLOCK, ROWLOCK)on the SELECT to acquire locks early and establish consistent lock ordering, which reduces deadlock risk.# 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})Implement retry logic for transient failures like deadlocks.
Example: Transfer funds (atomic operation)
This example demonstrates atomic transfer logic with lock hints to prevent lost updates in concurrent scenarios:
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
The lock hints WITH (UPDLOCK, ROWLOCK) on the SELECT ensure that the lock is acquired early. This prevents another transaction from reading the same balance concurrently and creating a lost update scenario where both transactions read the old balance, perform separate updates, and only the last update persists.
Temp table scoping
Temp tables (#tablename) are scoped to the session, but their creation is part of the current transaction. If you create a temp table and the transaction rolls back, the temp table is dropped:
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")
To keep a temp table independent of your data transaction, commit after creating it:
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 statements that require autocommit
Some DDL statements, such as CREATE DATABASE, ALTER DATABASE, and DROP DATABASE, can't run inside a transaction. Set autocommit=True before executing them:
conn.autocommit = True
cursor.execute("CREATE DATABASE TestDB")
conn.autocommit = False # Return to transactional mode
If you run CREATE DATABASE with autocommit=False, you get an error: CREATE DATABASE statement not allowed within multi-statement transaction.