mssql-pythonドライバーは、コミット、ロールバック、オートコミット設定、トランザクション分離レベルを含む完全なトランザクション制御をサポートしています。
トランザクションの基本
トランザクションは、一連のデータベース操作を単一の作業単位にまとめます。 トランザクションはACIDのプロパティに従います:
- 原子性:すべての操作は成功するか失敗するか。
- 一貫性:データベースは有効な状態のままです。
- 隔離:同時取引は互いに干渉しません。
- 耐久性:コミットした変更はシステムの故障を乗り越えます。
自動コミット モード
autocommit設定は変更が自動的にコミットされるかどうかを制御します。
オートコミット無効(デフォルト)
既定では autocommit=False です。 変更は明示的にコミットしなければなりません。
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()
コミットしなければ、接続が切断された際にドライバーは変更を破棄します。
オートコミット有効
autocommit=Trueを設定すると、ドライバーは各文を即座にコミットします:
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
オートコミットが有効になっていると、複数の文をグループとしてロールバックすることはできません。 オートコミットは、あなたのユースケースに合った場合にのみ使ってください。
コミットとロールバック
コミット
保留中の変更を恒久化するために commit() に連絡してください:
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() を呼び出してください:
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() と rollback() を呼び出すことができます:
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
カーソルレベルのコミットとロールバックは、呼び出したカーソルだけでなく、同じ接続上のすべてのカーソルに影響します。
コンテキスト マネージャー
接続コンテキストマネージャーはクリーン終了時にトランザクションをコミットし、例外が発生した場合はロールバックします。 終了時には接続は必ず切断されます。
autocommit=Trueを設定すると、コミットコールとロールバックコールは効果がありません。
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
例外が発生した場合、トランザクションはロールバックされます:
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
トランザクション分離レベル
アイソレーションレベルはトランザクションと同時トランザクションの相互作用を制御します。
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
)
利用可能な隔離レベル
| 定数 | 説明 |
|---|---|
SQL_TXN_READ_UNCOMMITTED |
他のトランザクションからのコミットされていない変更を読み取れます(ダーティリード許可) |
SQL_TXN_READ_COMMITTED |
コミットされたデータのみを読み込みます(SQL Serverのデフォルト) |
SQL_TXN_REPEATABLE_READ |
トランザクション内で一貫した読み込みを保証します |
SQL_TXN_SERIALIZABLE |
最も孤立している;トランザクションは順番に実行されているように見えます |
隔離レベルを選びましょう
| 利用シーン | 推奨レベル |
|---|---|
| 一般的なOLTPワークロード |
READ_COMMITTED (既定値) |
| 一貫したスナップショットが必要なレポート |
REPEATABLE_READ またはスナップショット |
| 正確さを求める財務計算 | SERIALIZABLE |
| 古いデータを許容する読み込み負荷の高いワークロード | READ_UNCOMMITTED |
スナップショットの分離
スナップショット分離には Transact-SQL(T-SQL)を使いましょう。 スナップショット分離はのtempdbを利用し、書き込み負荷が重い場合にストレージ要件を増加させることがあります。
# 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()
ネストされたトランザクションとセーブポイント
SQL Serverはトランザクション内の部分ロールバックのためのセーブポイントをサポートしています。
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")
デッドロックの処理
デッドロックは、2つのトランザクションがお互いのロックを待つときに発生します。 SQL Serverは自動的にデッドロックを検出し、1つのトランザクションを終了します。
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")
ベスト プラクティス
取引は短くして ロック期間やデッドロックの可能性を最小限に抑えましょう。
原子的であるべき多文トランザクションにはautocommit=Falseを使いましょう。
例外処理は常に ロールバックで行う:
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()コンテキストマネージャーを使って トランザクションを自動的に管理しましょう。 コンテキストマネージャーはクリーンエグジット時にコミットし、例外時にはロールバックします。
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安定性要件とパフォーマンス要件に基づいて適切なアイソレーションレベルを選択してください。
読み書き・修正・書き込みパターンにロックヒントを使い 、更新の喪失を防ぎましょう。 同じ取引内で更新される値を読み取ったら、SELECTの「
WITH (UPDLOCK, ROWLOCK)」などのヒントを使って早期にロックを取得し、一貫したロック順序を確立することで、デッドロックリスクを減らしましょう。# 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})デッドロックのような一時的な故障に対してリトライロジックを実装してください。
例:資金移送(原子力操作)
この例は、同時実行シナリオでの更新喪失を防ぐためのロックヒントを用いたアトミック転送ロジックを示しています:
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
SELECT のロック ヒント WITH (UPDLOCK, ROWLOCK) により、ロックが早期に取得されることが保証されます。 これにより、別のトランザクションが同じ残高を同時に読み取ることを防ぎ、両方のトランザクションが古い残高を読み込み、別々に更新を行い、最後の更新のみが残るという失われた更新シナリオが生まれます。
一時テーブルの有効範囲
一時テーブル(#tablename)はセッションにスコープが割り当てられていますが、その作成は現在のトランザクションの一部です。 一時テーブルを作成してトランザクションがロールバックすると、その一時テーブルは削除されます:
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")
一時テーブルをデータトランザクションから独立させたい場合は、作成後にコミットしてください:
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文
CREATE DATABASE、ALTER DATABASE、DROP DATABASEなどの一部のDDL文はトランザクション内で実行できません。 実行前に autocommit=True 設定:
conn.autocommit = True
cursor.execute("CREATE DATABASE TestDB")
conn.autocommit = False # Return to transactional mode
CREATE DATABASEでautocommit=Falseを実行するとエラーが出ます:CREATE DATABASE statement not allowed within multi-statement transaction.