本章では、VB.NET業務システムにおけるトランザクション設計とデータ整合性について学習します。
業務システムでは、単にデータを登録・更新できればよいわけではありません。 注文、請求、在庫、入金、承認、振込、契約、ポイント、権限変更など、複数のデータが関係する処理では、 途中で失敗した場合でもデータが壊れないように設計する必要があります。
たとえば、受注登録では、注文ヘッダ、注文明細、在庫引当、履歴ログをまとめて更新することがあります。 この途中でエラーが発生し、注文ヘッダだけ登録されて明細が登録されない状態になると、業務データとして不整合になります。
そのため、VB.NETの業務システムでは、ACID、トランザクション、楽観ロック、悲観ロック、排他制御、冪等性、リトライ設計、補償トランザクションなどを理解し、 業務データを安全に守る設計が必要です。
本章のゴールは、複雑な業務処理でもデータ不整合を起こさない設計ができるようになることです。
1. データ整合性とは
データ整合性とは、システム内のデータが業務ルールに対して矛盾なく保たれている状態を指します。
業務システムでは、1つの処理で複数のテーブルや外部システムを更新することが多くあります。 このとき、一部だけ成功し、一部だけ失敗すると、業務上矛盾したデータが残る可能性があります。
データ不整合の例
- 注文ヘッダは登録されたが、注文明細が登録されていない
- 在庫は減ったが、注文データが作成されていない
- 請求書は発行されたが、請求履歴が残っていない
- 入金処理は完了したが、残高が更新されていない
- 承認済みになったが、承認者情報が登録されていない
- 外部APIへ送信済みだが、自システムでは未送信扱いになっている
データ整合性を守るために必要な考え方
- 関連する更新をひとまとまりで扱う
- 途中失敗時にRollbackする
- 同時更新による上書きを防ぐ
- 二重登録・二重送信を防ぐ
- 処理の再実行性を確保する
- 外部連携失敗時の補償処理を設計する
- 更新結果をログや履歴に残す
データ整合性は、DBだけの問題ではありません。 アプリケーション設計、画面制御、バッチ処理、外部API、ファイル連携、運用手順まで含めて考える必要があります。
2. ACID
ACIDとは、トランザクションが満たすべき4つの性質を表す言葉です。 業務データを安全に扱ううえで非常に重要な考え方です。
| 性質 | 意味 | 業務システムでの例 |
|---|---|---|
| Atomicity | 原子性 | 注文ヘッダと明細は両方成功するか、両方失敗する |
| Consistency | 一貫性 | 在庫数がマイナスにならない |
| Isolation | 分離性 | 同時更新しても互いの処理が不正に干渉しない |
| Durability | 永続性 | Commit後のデータは障害後も保存される |
Atomicity:原子性
原子性とは、複数の処理をひとまとまりとして扱い、すべて成功するか、すべて失敗するかのどちらかにする性質です。
たとえば、注文登録では注文ヘッダと注文明細の両方が登録されて初めて業務上意味があります。 ヘッダだけ登録されて明細がない状態は不整合です。
Consistency:一貫性
一貫性とは、トランザクションの前後でデータが業務ルールを満たしていることです。 在庫数が0未満にならない、請求金額と明細合計が一致する、承認済みデータは編集できないなどが該当します。
Isolation:分離性
分離性とは、複数のトランザクションが同時に実行されても、互いに不正な影響を与えないことです。 同じ在庫を複数ユーザーが同時に購入した場合でも、在庫数が矛盾しないようにする必要があります。
Durability:永続性
永続性とは、一度Commitされたデータは、障害が発生しても失われないことです。 DBMSはログや永続化機構によってこの性質を保証します。
3. トランザクション制御の基本
トランザクション制御では、複数のDB更新をひとまとまりとして扱います。 VB.NETでは、SqlTransactionを使って明示的にトランザクションを制御できます。
基本的なトランザクション処理
Using connection As New SqlClient.SqlConnection(_connectionString)
connection.Open()
Using transaction As SqlClient.SqlTransaction = connection.BeginTransaction()
Try
InsertOrderHeader(connection, transaction, order)
InsertOrderDetails(connection, transaction, order.Details)
UpdateStock(connection, transaction, order.Details)
transaction.Commit()
Catch ex As Exception
transaction.Rollback()
Throw
End Try
End Using
End Using
この例では、注文ヘッダ登録、注文明細登録、在庫更新を1つのトランザクションとして扱っています。 途中で例外が発生した場合はRollbackされ、すべての更新が取り消されます。
CommandにTransactionを設定する
Using command As New SqlClient.SqlCommand(sql, connection, transaction)
command.Parameters.Add("@OrderId", SqlDbType.Int).Value = orderId
command.ExecuteNonQuery()
End Using
同じトランザクション内でSQLを実行する場合、SqlCommandに同じConnectionとTransactionを渡す必要があります。 Transactionを設定し忘れると、意図したトランザクションに含まれない可能性があります。
トランザクションが必要な処理
- ヘッダ・明細を同時に登録する処理
- 注文登録と在庫更新
- 請求データ作成と請求履歴登録
- 承認処理と承認ログ登録
- 入金処理と残高更新
- ユーザー作成と権限付与
- 複数テーブルにまたがる削除処理
関連するデータがすべてそろって初めて業務上意味を持つ場合は、トランザクション制御を検討します。
4. トランザクション範囲の設計
トランザクションはデータ整合性を守るために重要ですが、範囲を広げすぎると性能問題やロック競合の原因になります。
悪い例:外部処理までトランザクションに含める
Using transaction = connection.BeginTransaction()
InsertOrder(connection, transaction, order)
ExportPdf(order)
SendMail(order)
CallExternalApi(order)
transaction.Commit()
End Using
この例では、PDF出力、メール送信、外部API通信がトランザクション内に含まれています。 これらの処理が遅い場合、その間DBロックが保持され、他のユーザーやバッチ処理に影響します。
改善例:DB更新と外部処理を分離する
Using transaction = connection.BeginTransaction()
InsertOrder(connection, transaction, order)
InsertSendQueue(connection, transaction, order)
transaction.Commit()
End Using
' Commit後にキューをもとに外部処理を行う
ProcessSendQueue(order.OrderId)
DB更新と外部処理を分離し、外部処理は送信キューや後続バッチで行う設計にすると、トランザクション時間を短くできます。
トランザクション範囲設計のポイント
- DB更新に必要な最小範囲にする
- ユーザー操作待ちを含めない
- 外部API通信を含めない
- ファイル出力やメール送信を含めない
- 大量更新は分割を検討する
- ロック時間を短くする
トランザクションは長ければ安全というものではありません。 短く、明確で、業務上必要な範囲に限定することが重要です。
5. 楽観ロック
楽観ロックとは、「同時更新は頻繁には起きない」という前提で、更新時に他のユーザーが先に変更していないかを確認する排他制御です。
業務画面では、ユーザーAとユーザーBが同じデータを開き、後から保存した人の内容で上書きされる問題が起きることがあります。 楽観ロックを使うと、このような上書き更新を防げます。
Version列を使った楽観ロック
Orders
- OrderId
- Status
- TotalAmount
- Version
データ取得時にVersionを画面側へ保持し、更新時に同じVersionであることを条件にします。
楽観ロック付きUPDATE
UPDATE Orders
SET
Status = @Status,
TotalAmount = @TotalAmount,
Version = Version + 1
WHERE OrderId = @OrderId
AND Version = @OriginalVersion
このSQLの更新件数が0件の場合、他のユーザーによって先に更新された可能性があります。
VB.NETでの判定例
Dim affectedRows As Integer = command.ExecuteNonQuery()
If affectedRows = 0 Then
Throw New BusinessException("他のユーザーによって更新されています。再読み込みしてください。")
End If
楽観ロックが向いているケース
- 同時更新がそれほど多くない
- ユーザーが画面でデータを編集する
- 後勝ち上書きを防ぎたい
- 長時間ロックを保持したくない
- Webアプリやデスクトップ画面の更新処理
楽観ロックの注意点
- 更新エラー時の利用者メッセージを設計する
- 画面再読み込みの導線を用意する
- Version列やUpdatedAt列を確実に保持する
- 複数テーブル更新時の整合性を考慮する
6. 悲観ロック
悲観ロックとは、「同時更新が起きる可能性が高い」という前提で、先にデータをロックし、他の処理が更新できないようにする方式です。
在庫引当、採番、金融取引、座席予約、同時操作が多い重要データなどでは、悲観ロックが必要になることがあります。
悲観ロックの考え方
1. 対象データをロックして取得する
2. 業務チェックを行う
3. 更新する
4. CommitまたはRollbackでロックを解放する
SQL Serverでのロック取得例
SELECT
StockId,
Quantity
FROM Stocks WITH (UPDLOCK, ROWLOCK)
WHERE ProductId = @ProductId
UPDLOCKを使うことで、更新予定のロックを取得し、他の更新処理との競合を制御します。
悲観ロックが向いているケース
- 同時更新が頻繁に発生する
- 在庫や残高など絶対に壊せないデータを扱う
- 二重引当や二重予約を防ぎたい
- 更新前チェックと更新を厳密に一体化したい
悲観ロックの注意点
- ロック時間が長いと待ちが発生する
- デッドロックの原因になる可能性がある
- トランザクション範囲を短くする必要がある
- ユーザー操作待ちを含めてはいけない
- 更新順序を統一する
悲観ロックは強力ですが、使い方を誤るとシステム全体の応答性を悪化させます。 本当に必要な箇所に限定して使うことが重要です。
7. 排他制御
排他制御とは、複数の処理が同じデータを同時に変更して不整合を起こさないように制御する仕組みです。
排他制御には、アプリケーション側で行うもの、DB側で行うもの、業務ルールとして行うものがあります。
排他制御の種類
| 方式 | 内容 | 例 |
|---|---|---|
| 楽観ロック | 更新時に変更有無を確認する | Version列、UpdatedAt列 |
| 悲観ロック | 先にロックして他処理を待たせる | UPDLOCK、SELECT FOR UPDATE |
| 業務ロック | 処理中フラグやロックテーブルで制御する | 締め処理中フラグ |
| アプリ内ロック | 同一プロセス内で同時実行を制御する | SyncLock |
業務ロックの例
月次締め処理中は、対象月の売上データを画面から更新できないようにするケースがあります。 このような場合、業務ロック用の管理テーブルを作ることがあります。
ProcessLocks
- LockKey
- LockedBy
- LockedAt
- ExpiresAt
業務ロック利用例
If _processLockRepository.Exists("MonthlyClosing_2026_08") Then
Throw New BusinessException("月次締め処理中のため更新できません。")
End If
業務ロックは、DBの物理的なロックとは異なり、業務ルールとして処理を制御する仕組みです。 長時間処理やバッチ処理では有効な場合があります。
排他制御設計のポイント
- 何を同時実行させてはいけないかを明確にする
- 楽観ロックで十分か、悲観ロックが必要か判断する
- 長時間のDBロックを避ける
- 業務ロックの解除漏れに注意する
- ロック取得失敗時のメッセージを設計する
- ログにロック取得・解除を記録する
8. 分散トランザクション
分散トランザクションとは、複数のDBや外部システムをまたいで一貫性を保とうとするトランザクションです。
たとえば、自社DBを更新しながら、外部決済システムや外部在庫システムも同時に更新する場合、すべてを完全に同じトランザクションで扱いたくなることがあります。
分散トランザクションが問題になりやすい例
- 自社DB更新と外部API送信
- 複数DBへの同時更新
- ファイル出力とDB更新
- メール送信とDB更新
- 外部決済と注文確定
しかし、現実には外部API、メール、ファイル出力などはDBトランザクションのように簡単にRollbackできません。 メールは送信後に取り消せませんし、外部APIの処理結果も自社DBのRollbackと連動しないことがあります。
悪い例:外部APIとDB更新を一体化して考える
Using transaction = connection.BeginTransaction()
SaveOrder(connection, transaction, order)
CallPaymentApi(order)
transaction.Commit()
End Using
この例では、決済API呼び出し後にDB Commitで失敗した場合、外部決済は成功したのに自社DBには注文が存在しない状態になる可能性があります。
現実的な設計方針
- 外部システムとの完全な同時Commitを前提にしない
- 処理状態をDBに記録する
- 送信キューやイベントテーブルを使う
- リトライ可能にする
- 失敗時の補償処理を用意する
- 運用で確認できる管理画面を用意する
送信キュー方式の例
1. 自社DBに注文を登録する
2. 同じトランザクションで送信キューへ登録する
3. Commitする
4. 別処理が送信キューを読み取る
5. 外部APIへ送信する
6. 成功・失敗を送信キューに記録する
この方式では、自社DBの整合性を保ちながら、外部連携を再実行可能な形にできます。
9. 二重登録防止
二重登録とは、同じ業務データが複数回登録されてしまう問題です。 ユーザーの二度押し、通信リトライ、バッチ再実行、外部APIの再送などで発生します。
二重登録が起きる原因
- 登録ボタンを連打する
- 処理中に画面が固まり、再度実行する
- 通信タイムアウト後に再送する
- バッチを再実行する
- 外部システムから同じデータが複数回来る
画面側での二重実行防止
Private Async Sub btnRegister_Click(sender As Object, e As EventArgs) Handles btnRegister.Click
btnRegister.Enabled = False
Try
Await _registerService.RegisterAsync(request)
MessageBox.Show("登録しました。")
Finally
btnRegister.Enabled = True
End Try
End Sub
ボタンを無効化することで、ユーザーの二度押しを防げます。 ただし、画面制御だけでは不十分です。 DB側やService側でも二重登録を防ぐ必要があります。
一意制約による防止
CREATE UNIQUE INDEX UX_Orders_RequestId
ON Orders (RequestId)
RequestIdのような一意なキーを用意し、同じリクエストが二度登録されないようにします。
Service側での防止例
If _orderRepository.ExistsByRequestId(request.RequestId) Then
Return _orderRepository.FindByRequestId(request.RequestId)
End If
_orderRepository.Save(order)
すでに同じRequestIdで登録済みの場合は、新規登録せず既存データを返します。 これにより、リトライ時にも二重登録を防げます。
10. 冪等性
冪等性とは、同じ処理を複数回実行しても結果が変わらない性質です。 外部API連携、バッチ処理、リトライ処理、キュー処理では非常に重要です。
冪等でない処理
Public Sub AddPoint(userId As Integer, point As Integer)
' 呼ばれるたびにポイントが加算される
End Sub
この処理は、同じリクエストが2回実行されるとポイントが2回加算されます。 そのため、リトライに弱い設計です。
冪等な処理の考え方
Public Sub AddPointOnce(userId As Integer, point As Integer, requestId As String)
If _pointHistoryRepository.Exists(requestId) Then
Return
End If
_pointRepository.Add(userId, point)
_pointHistoryRepository.Save(requestId, userId, point)
End Sub
requestIdを使って処理済みかどうかを判定することで、同じ処理が複数回実行されてもポイント加算は1回だけになります。
冪等性が必要な処理
- 外部APIからの受信処理
- 決済処理
- ポイント付与
- 在庫引当
- メール送信
- バッチ再実行
- ファイル取込
冪等性を実現する方法
- リクエストIDを使う
- 処理履歴テーブルを作る
- 一意制約を利用する
- 状態遷移を明確にする
- 同じ入力に対して同じ結果を返す
- 二重実行時の扱いを仕様化する
リトライ設計を行う場合、冪等性がない処理を安易に再実行すると、二重登録や二重加算が発生します。 リトライと冪等性は必ずセットで考える必要があります。
11. リトライ設計
リトライ設計とは、一時的な失敗が発生した場合に、一定条件のもとで処理を再実行する設計です。 ネットワーク障害、タイムアウト、デッドロック、一時的な外部API障害などで有効な場合があります。
リトライしてよい可能性があるエラー
- 一時的なネットワークエラー
- 外部APIの一時的なタイムアウト
- DBデッドロック
- 一時的な接続失敗
- ファイルロックによる一時失敗
リトライすべきでないエラー
- 入力値不正
- 認証エラー
- 権限不足
- 業務ルール違反
- SQL構文エラー
- データ不整合
リトライ処理の例
Public Sub ExecuteWithRetry(action As Action)
Dim maxRetryCount As Integer = 3
For retryCount As Integer = 1 To maxRetryCount
Try
action()
Return
Catch ex As TimeoutException
Logger.Warn("タイムアウトが発生しました。Retry=" & retryCount)
If retryCount = maxRetryCount Then
Throw
End If
Threading.Thread.Sleep(1000 * retryCount)
End Try
Next
End Sub
リトライ設計のポイント
- リトライ対象の例外を限定する
- 最大リトライ回数を決める
- リトライ間隔を決める
- リトライ履歴をログに残す
- 冪等性を確保する
- 失敗時に運用で確認できるようにする
リトライは便利ですが、無制限に実行してはいけません。 障害時に外部システムへ負荷をかけ続けたり、二重処理を発生させたりする危険があります。
12. 補償トランザクション
補償トランザクションとは、すでに完了してしまった処理に対して、逆方向の処理を行うことで業務的に整合性を回復する考え方です。
DBトランザクションのRollbackとは異なり、外部API、メール送信、決済、配送依頼など、取り消しが必要な処理に対して使われます。
補償トランザクションが必要な例
- 決済成功後に注文登録が失敗したため、返金処理を行う
- 在庫引当後に注文確定が失敗したため、在庫引当を解除する
- 外部システムへ登録後に自社処理が失敗したため、外部システムへ取消要求を送る
- 配送依頼後にキャンセルされたため、配送キャンセルAPIを呼ぶ
補償処理の例
Try
ReserveStock(order)
ChargePayment(order)
ConfirmOrder(order)
Catch ex As Exception
Logger.Error("注文確定に失敗しました。補償処理を開始します。", ex)
CancelPayment(order)
ReleaseStock(order)
Throw
End Try
この例では、注文確定に失敗した場合に、決済取消と在庫引当解除を行います。
補償トランザクション設計のポイント
- 何を取り消せるかを事前に確認する
- 取り消せない処理は状態管理で補う
- 補償処理自体の失敗も考慮する
- 補償処理の履歴を残す
- 手動対応が必要な状態を管理する
- 利用者や運用担当者が確認できるようにする
外部連携を含む業務処理では、完全なRollbackができないことを前提に設計する必要があります。 そのため、補償処理、状態管理、運用確認が重要になります。
13. 状態管理とステータス設計
複雑な業務処理では、現在どの状態にあるのかを明確に管理することが重要です。 状態管理が曖昧だと、二重実行、再実行不能、処理漏れ、不整合が発生しやすくなります。
注文処理の状態例
| ステータス | 意味 | 可能な操作 |
|---|---|---|
| Draft | 下書き | 編集、申請、削除 |
| Applying | 申請中 | 承認、差戻し |
| Approved | 承認済み | 注文確定 |
| Confirmed | 確定済み | 出荷依頼、キャンセル |
| Canceled | キャンセル済み | 参照のみ |
Enumで状態を表現する
Public Enum OrderStatus
Draft
Applying
Approved
Confirmed
Canceled
End Enum
状態遷移をDomainに閉じ込める
Public Class Order
Public Property Status As OrderStatus
Public Sub Approve()
If Status <> OrderStatus.Applying Then
Throw New BusinessException("申請中の注文のみ承認できます。")
End If
Status = OrderStatus.Approved
End Sub
Public Sub Cancel()
If Status = OrderStatus.Canceled Then
Throw New BusinessException("既にキャンセル済みです。")
End If
If Status = OrderStatus.Confirmed Then
Throw New BusinessException("確定済み注文は通常キャンセルできません。")
End If
Status = OrderStatus.Canceled
End Sub
End Class
状態変更を単なる代入にすると、不正な状態遷移が発生しやすくなります。 Domainモデルに状態変更メソッドを用意し、業務ルールを守るようにします。
ステータス設計のポイント
- 状態一覧を明確にする
- 状態ごとに可能な操作を定義する
- 不正な状態遷移を防ぐ
- 状態変更履歴を残す
- 処理中・失敗・要確認などの運用状態も考慮する
14. 実務でありがちなトランザクション設計の問題
トランザクションがない
複数テーブルを更新しているにもかかわらず、トランザクションがない場合、途中失敗で不整合が発生します。
トランザクション範囲が広すぎる
外部API、メール送信、ファイル出力、ユーザー操作待ちをトランザクションに含めると、ロック時間が長くなり性能問題につながります。
楽観ロックがない
複数ユーザーが同じデータを編集する画面で楽観ロックがない場合、後から保存した内容で上書きされる可能性があります。
リトライで二重登録している
冪等性を考慮せずにリトライすると、注文、ポイント、決済、メール送信などが二重に実行される可能性があります。
外部APIをRollbackできる前提で設計している
外部APIやメール送信は、DBのRollbackと同じようには取り消せません。 状態管理、送信キュー、補償処理を設計する必要があります。
状態遷移が単なる文字列更新になっている
Status = “承認済み” のような直接更新が複数箇所に散らばると、不正な状態遷移を防げません。 状態変更はDomainモデルやServiceに集約します。
15. 演習課題
演習1:受注登録のトランザクションを設計する
注文ヘッダ、注文明細、在庫引当、操作ログを登録する処理を想定し、 どこからどこまでを1つのトランザクションにするべきか設計してください。
演習2:楽観ロックを実装する
OrdersテーブルにVersion列を追加し、他ユーザーが先に更新した場合にエラーとするUPDATE文とVB.NET側の判定処理を実装してください。
演習3:悲観ロックを使った在庫引当を設計する
在庫数を取得してから引当数を減算する処理を、悲観ロックを使って安全に実装してください。
演習4:二重登録防止を実装する
RequestIdを使って、同じ登録処理が複数回実行されても注文が二重登録されないようにしてください。 DB側の一意制約も設計してください。
演習5:冪等なポイント付与処理を作る
同じリクエストIDで複数回ポイント付与処理が呼ばれても、ポイントが1回だけ加算されるように設計してください。
演習6:補償トランザクションを設計する
注文確定、在庫引当、決済、配送依頼を含む処理を想定し、 途中失敗時にどの補償処理を行うべきか整理してください。
演習7:状態遷移表を作成する
注文、請求、承認のいずれかを題材にし、 ステータス一覧、可能な操作、不正な操作、状態変更履歴の設計を行ってください。
16. まとめ
本章では、VB.NET業務システムにおけるトランザクション設計とデータ整合性について学習しました。
業務システムでは、注文、請求、在庫、入金、承認、権限変更など、複数のデータをまとめて更新する処理が多くあります。 このような処理では、一部だけ成功して一部だけ失敗する状態を防ぐために、適切なトランザクション制御が必要です。
ACIDは、トランザクション設計の基本となる考え方です。 原子性、一貫性、分離性、永続性を理解することで、業務データを安全に扱う設計ができます。
トランザクション範囲は、広ければ安全というものではありません。 DB更新に必要な最小範囲に限定し、外部API、メール送信、ファイル出力、ユーザー操作待ちなどは原則としてトランザクション外へ分離します。
同時更新を防ぐためには、楽観ロックや悲観ロックを適切に使い分ける必要があります。 画面編集では楽観ロックが有効な場合が多く、在庫や残高など厳密な同時更新制御が必要な処理では悲観ロックを検討します。
外部APIやメール送信、ファイル出力などを含む処理では、DBトランザクションだけでは整合性を守れません。 送信キュー、状態管理、リトライ、補償トランザクションを組み合わせる必要があります。
二重登録防止や冪等性も、本番運用では非常に重要です。 ユーザーの二度押し、通信リトライ、バッチ再実行、外部システムからの再送に備え、 RequestId、一意制約、処理履歴テーブルなどを使って安全に再実行できる設計にします。
また、複雑な業務ではステータス設計が重要です。 状態一覧、可能な操作、不正な遷移、状態変更履歴を明確にし、状態変更ロジックをDomainモデルやServiceへ集約することで、不整合を防ぎやすくなります。
本章のゴールは、複雑な業務処理でもデータ不整合を起こさない設計ができるようになることです。 トランザクション設計は、DB技術だけでなく、業務ルール、運用、外部連携、障害対応を含めた総合的な設計スキルです。