【テクニカル・上級編】DAOのUpdateメソッドとADOのUpdateメソッド:コミットのタイミングとキャッシュ制御 – Access VBA解析バイブル

スポンサーリンク

なぜあなたのシステムは「Update」したはずのデータを喪失するのか

基幹系業務システムの保守現場において、稀に、しかし確実に発生する怪奇現象がある。「画面上では正常に更新完了メッセージが表示され、エラーもトラップされなかったにもかかわらず、直後にクライアントPCがフリーズ、あるいは瞬電した際、データが跡形もなくロールバック(消失)している」というインシデントだ。

ジュニアエンジニアは首を傾げ、「OSのクラッシュが原因です」と報告書をまとめるかもしれない。だが、我々チーフアーキテクトの診断は異なる。それはDAOおよびADOにおける「Update」という操作の抽象化レイヤーを履き違え、OSおよびストレージエンジンへのフラッシュ(Flush)制御を怠った結果による必然のデータ欠損である。

VBAにおいて、`Recordset.Update` を呼ぶことは「磁気ディスクやSSDセルにデータが刻まれた」ことと同義ではない。それは単に「メモリ上の作業バッファ(Copy Buffer)から、DBエンジン(ACE/Jet)またはOLE DBプロバイダの内部キャッシュに行データが移動した」に過ぎない。

本稿では、DAOとADOという2大データアクセスコンポーネントが内部で保持するキャッシュアーキテクチャの根本的な違いを解体し、意図せぬ電源断やプロセスハングからデータを守り抜くための「物理コミットの極限制御」を詳解する。

—

1. DAOとADOのキャッシュアーキテクチャの構造的差異

両者の挙動を分かつ本質は、「誰がバッファを持ち、誰がディスクI/Oを制御しているか」というアーキテクチャの階層数にある。

【DAO アーキテクチャ】
[VBA Application]
│
[DAO 3.6 / ACE Core] ─── (共有メモリキャッシュ / Page Allocation)
│
[OS File System Cache (Windows)]
│
[Physical Disk (HDD/SSD)]

【ADO アーキテクチャ】
[VBA Application]
│
[ADO Recordset (Cursor Engine)] ─── (行単位/バッチバッファ)
│
[OLE DB Provider (Microsoft.ACE.OLEDB)]
│
[ACE/Jet Core Engine]
│
[OS File System Cache (Windows)]
│
[Physical Disk (HDD/SSD)]

DAO(Data Access Objects):Jet/ACE直結の近道

DAOは、Microsoft AccessのコアエンジンであるACE(旧Jet)と同一プロセス内で密結合している。

  • `Recordset.Edit` で該当データページのロックおよびワークバッファの展開を行う。
  • `Recordset.Update` を実行した瞬間、データは即座にACEエンジンの共有ページバッファ(Page-level cache)へと反映される。
  • しかし、この時点でもまだOSのファイルシステムキャッシュにすら書き込まれていない可能性がある。ACEエンジンはスループットを稼ぐため、デフォルトでWrite-Behind Cache(遅延書き込み)を採用しているからだ。

ADO(ActiveX Data Objects):多層抽象化によるブラックボックス

ADOはOLE DBという汎用データアクセスレイヤーの上に構築された抽象化ラッパーである。

  • クライアントサイドカーソル(`adUseClient`)を使用している場合、`Update` メソッドはローカルメモリ上の行セット(Client Cursor Engine)を更新するに過ぎない。
  • `UpdateBatch` を呼ぶことで初めてOLE DBプロバイダを経由し、背後のACEエンジンにデータが送られる。
  • サーバーサイドカーソル(`adUseServer`)であっても、OLE DBプロバイダ層が介在するため、DAOに比べて「いつ物理ディスクに届くか」の予測が極めて困難になる。

—

2. DAOのUpdateとコミット制御:dbForceOSFlushの絶対的威力

DAOを使用している場合、真のデータ永続化(Durability)を保証するための鍵は、`Recordset.Update` 単体ではなく、「明示的トランザクション」と「`dbForceOSFlush` 定数」の組み合わせにある。

遅延書き込みの罠と `dbForceOSFlush`

通常、DAOの `Workspace.CommitTrans` は非同期で処理される。つまり、ACEエンジンは「書き込み要求を受け取った」時点で制御をVBAに戻し、ディスクへの物理I/Oはバックグラウンドスレッド(5秒間隔等のアイドル時)で行う。この数秒の間にハードウェア障害やブルースクリーンが発生すれば、データは虚無へと消え去る。

これを防ぐ唯一の手段が、ACE内部からWin32 APIの `FlushFileBuffers` を間接的にコールさせる `dbForceOSFlush` フラグである。

‘ 通常のコミット(OSキャッシュへの書き込みは遅延される)
ws.CommitTrans

‘ 物理I/Oを同期実行させるコミット(OSとディスクコントローラへフラッシュを強制)
ws.CommitTrans dbForceOSFlush

`dbForceOSFlush` を指定すると、ACEエンジンはOSに対して同期ファイルフラッシュを要求し、ストレージコントローラが書き込み完了応答を返すまでプロセスの実行をブロックする。

—

3. ADOのUpdateとコミット制御:プロバイダプロパティの調律

ADO環境(`ADODB.Connection` および `ADODB.Recordset`)では、DAOのような `dbForceOSFlush` フラグは引数として露出していない。ADOで同様の耐障害性を得るには、接続文字列(Connection String)レベルでOLE DBプロバイダの内部動作を調律する必要がある。

`Jet OLEDB:Transaction Commit Mode`

Microsoft.ACE.OLEDB プロバイダには、非公開に近い拡張プロパティが存在する。

Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\Database\CoreSystem.accdb;Jet OLEDB:Transaction Commit Mode=1;

  • `Jet OLEDB:Transaction Commit Mode=1`: 同期コミットモード。トランザクションのコミット時に物理ディスクへのフラッシュを試みる。
  • `Jet OLEDB:Transaction Commit Mode=2`: 遅延コミットモード(デフォルト)。スループットは向上するがクラッシュ時の堅牢性は失われる。

ADOでクリティカルな金銭トランザクション等を扱う場合は、このプロバイダオプションの明示的指定が必須となる。

—

4. 極限の同期書き込みを実現する堅牢化コード

ここでは、DAOを用いた完全同期トランザクション処理の実装例を示す。
万全を期すため、DAOの `dbForceOSFlush` を利用しつつ、必要に応じて低レイヤーのWin32 APIを組み合わせたエンタープライズ水準の堅牢化プロシージャである。

実装:確定書き込みを強制する高信頼性DAOトランザクション

Option Compare Database
Option Explicit

‘ ==============================================================================
‘ モジュール名: mod_RobustPersistence
‘ 責務: データベースへの絶対的な物理書き込み(Durability)を保証する実行基盤
‘ ==============================================================================

If VBA7 Then
Private Declare PtrSafe Function FlushFileBuffers Lib “kernel32” (ByVal hFile As LongPtr) As Long
Private Declare PtrSafe Function CloseHandle Lib “kernel32” (ByVal hObject As LongPtr) As Long
Private Declare PtrSafe Function CreateFileW Lib “kernel32” ( _
ByVal lpFileName As LongPtr, _
ByVal dwDesiredAccess As Long, _
ByVal dwShareMode As Long, _
ByVal lpSecurityAttributes As LongPtr, _
ByVal dwCreationDisposition As Long, _
ByVal dwFlagsAndAttributes As Long, _
ByVal hTemplateFile As LongPtr) As LongPtr
Else
Private Declare Function FlushFileBuffers Lib “kernel32” (ByVal hFile As Long) As Long
Private Declare Function CloseHandle Lib “kernel32” (ByVal hObject As Long) As Long
Private Declare Function CreateFileW Lib “kernel32″ ( _
ByVal lpFileName As Long, _
ByVal dwDesiredAccess As Long, _
ByVal dwShareMode As Long, _
ByVal lpSecurityAttributes As Long, _
ByVal dwCreationDisposition As Long, _
ByVal dwFlagsAndAttributes As Long, _
ByVal hTemplateFile As Long) As Long
End If

Private Const GENERIC_READ As Long = &H80000000
Private Const GENERIC_WRITE As Long = &H40000000
Private Const FILE_SHARE_READ As Long = &H1
Private Const FILE_SHARE_WRITE As Long = &H2
Private Const OPEN_EXISTING As Long = 3
Private Const FILE_ATTRIBUTE_NORMAL As Long = &H80
Private Const INVALID_HANDLE_VALUE As Long = -1

”’

”’ クリティカルな台帳更新を同期的に実行し、ディスクフラッシュを完了させる
”’

Public Sub ExecuteCriticalTransaction(ByVal targetId As Long, ByVal newAmount As Currency)
Dim ws As DAO.Workspace
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim isTransactionActive As Boolean

On Error GoTo Err_Handler

‘ 1. デフォルトワークスペースとデータベースの捕捉
‘ CurrentDb() は都度新しいインスタンス参照を生成するため、明示的オブジェクト変数に束縛する
Set ws = DBEngine.Workspaces(0)
Set db = CurrentDb()

‘ 2. トランザクション境界の開始
ws.BeginTrans
isTransactionActive = True

‘ dbOpenDynaset, dbSeeChanges は排他制御とSQL Server移行を見据えた標準指定
Set rs = db.OpenRecordset(“SELECT ID, Amount, UpdatedAt FROM T_Ledger WHERE ID = ” & targetId, _
dbOpenDynaset, dbSeeChanges)

If Not rs.EOF Then
rs.Edit
rs.Fields(“Amount”).Value = newAmount
rs.Fields(“UpdatedAt”).Value = Now()

‘ Recordset.Update 自体はACEの内部ページキャッシュを更新するのみ
rs.Update
Else
Err.Raise vbObjectError + 1001, “ExecuteCriticalTransaction”, “対象レコードが存在しません: ID=” & targetId
End If

‘ Recordsetを即座にクローズし、内部作業リソースを解放
rs.Close
Set rs = Nothing

‘ 3. dbForceOSFlush による物理コミット
‘ OSファイルシステムキャッシュの遅延書き込みを許容せず、物理層へフラッシュを要求
ws.CommitTrans dbForceOSFlush
isTransactionActive = False

‘ 4. ガベージコレクションと参照カウントの明示的破棄
Set db = Nothing
Set ws = Nothing

Exit Sub

Err_Handler:
Dim errNum As Long
Dim errDesc As String
errNum = Err.Number
errDesc = Err.Description

‘ 異常終了時は確実にロールバック
If isTransactionActive Then
ws.Rollback
End If

‘ オブジェクトの安全なクリーンアップ
If Not rs Is Nothing Then
On Error Resume Next
rs.Close
Set rs = Nothing
On Error GoTo 0
End If
Set db = Nothing
Set ws = Nothing

‘ 上位層へ再スロー
Err.Raise errNum, “ExecuteCriticalTransaction”, “トランザクション中断: ” & errDesc
End Sub

”’

”’ (補足)バックエンドMDB/ACCDBファイル自体に対して直接Win32レベルのフラッシュを強制する
”’

Public Sub HardFlushDatabaseFile(ByVal dbPath As String)
#If VBA7 Then
Dim hFile As LongPtr
#Else
Dim hFile As Long
#End If

‘ 排他を奪わないよう FILE_SHARE_READ Or FILE_SHARE_WRITE でハンドルを取得
hFile = CreateFileW(StrPtr(dbPath), _
GENERIC_READ Or GENERIC_WRITE, _
FILE_SHARE_READ Or FILE_SHARE_WRITE, _
0&, _
OPEN_EXISTING, _
FILE_ATTRIBUTE_NORMAL, _
0&)

If hFile <> INVALID_HANDLE_VALUE Then
‘ Windowsのファイルバッファを物理ディスクにフラッシュ
Call FlushFileBuffers(hFile)
Call CloseHandle(hFile)
End If
End Sub

—

5. チーフアーキテクトが下す設計判断:パフォーマンスと堅牢性のトレードオフ

同期フラッシュ(`dbForceOSFlush`)の導入には代償が伴う。それは「絶対的スループットの低下」である。

| 観点 | `Recordset.Update` のみ | トランザクション + `dbForceOSFlush` |
| :— | :— | :— |
| I/Oモデル | 完全非同期(Write-Behind) | 完全同期(Blocking I/O) |
| ディスク書込 | OSとACEのアイドル時に遅延書込 | ストレージの確認応答まで待機 |
| 電源断時の損失 | 直近数秒分のデータが消失するリスク | コミット完了分は100%復旧可能 |
| 1秒あたりの処理件数 | 数千 ~ 数万件 | 数十 ~ 数百件(I/O性能に依存) |

設計上の鉄則

1. ループ内で `dbForceOSFlush` を呼ぶな
1万行のCSVをインポートする際、1行ごとに `CommitTrans dbForceOSFlush` を実行してはならない。磁気ヘッドやSSDコントローラへの同期待ちが1万回発生し、処理時間は数十倍に跳ね上がる。
必ず 100~1,000レコード単位のバルク単位でトランザクション境界を切り、その境界でのみフラッシュを実行する 設計とすること。
2. CurrentDb() の乱用に終止符を打て
`CurrentDb.Execute` や `CurrentDb.OpenRecordset` をインラインで呼び出すと、その瞬間にDAOの内部DBインスタンスが生成され、破棄される。これは内部キャッシュの局所性を破壊し、不必要なメモリの確保と破棄を繰り返す。必ずモジュール変数やプロシージャ先頭で単一の `DAO.Database` インスタンスへ参照を束縛し、最後に `Set db = Nothing` で明示的に決定論的破棄を行わなければならない。

—

結言

Access VBAという環境は、その親しみやすさゆえに、下層のストレージアーキテクチャを忘却させがちである。
しかし、OSから見ればAccessもまた、WindowsのWin32サブシステム上でI/Oリクエストを発行する一介のプロセスに過ぎない。

「Updateしたはずのデータが消えた」と嘆く前に、自分が扱っているオブジェクトがDAOなのかADOなのか、そしてそのUpdateがメモリに対する更新なのか、OSバッファに対する更新なのか、あるいは物理ディスクへの書き込み完了なのかを意識しなければならない。

この階層構造を完全に脳内にマッピングし、適切なフラッシュ戦略を選択できる者だけが、20年稼働し続ける堅牢なエンタープライズ・レガシーを構築できるのである。

タイトルとURLをコピーしました