DAO.RecordsetのBatchUpdateを自作する:トランザクション管理による一括更新の極意
開発現場で、Access VBAによるデータ処理の遅さに絶望したことはないだろうか。
「数万件のレコードを更新するのに数分もかかる」「画面がフリーズしたようになる」。もし、あなたがループの中で1件ずつ`MoveNext`しながら`.Edit`と`.Update`を叩いているなら、今すぐその実装を止めてほしい。それはAccessのエンジン(ACE/Jet)に対して、最も残酷な負荷をかけ続けている行為に他ならない。
ADOにはバッチ更新モード(`CursorLocation = adUseClient` 等)が存在するが、我らがAccessのネイティブ領域であるDAO(Data Access Objects)には、明示的な「BatchUpdate」メソッドは用意されていない。
しかし、嘆く必要はない。DAOが持つトランザクションの強制制御とメモリキャッシュの特性を熟知していれば、自手で圧倒的な高速バッチ更新(BatchUpdate)の仕組みを構築することは容易い。
今回は、実務の現場で「使える」レベルを超越した、堅牢かつ爆速のトランザクション一括更新アーキテクチャを伝授する。
—
なぜレコード単位の更新は「悪」なのか?
非効率なコードがなぜ遅いのか、そのメカニズムをアーキテクトの視点から紐解こう。
‘ 【アンチパターン】絶対にやってはいけないループ更新
Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset(“T_LargeData”, dbOpenDynaset)
Do While Not rs.EOF
rs.Edit
rs!Status = “Processed”
rs.Update ‘ ←ここで毎回ディスクI/Oとトランザクションログ書き込みが発生!
rs.MoveNext
Loop
rs.Close
このコードの何が問題か。
Accessのデフォルト挙動では、`.Update`メソッドが実行された瞬間、その変更内容がストレージ上のデータベースファイルへ書き込まれ、暗黙のトランザクションコミットが発生する。つまり、1万件のレコードがあれば、1万回のディスク書き込みとロック制御のオーバーヘッドが発生していることになる。これでは遅くて当然だ。
解決のアプローチ:トランザクションの包摂とメモリ上での完結
データベースのパフォーマンスを極限まで引き出す鉄則は以下の2点である。
1. トランザクションの範囲を広げ、ディスクI/Oの回数を物理的限界まで減らす。
2. エラーハンドリングを完璧にし、途中で失敗した場合は一網打尽にロールバック(Rollback)する。
これをDAOのトランザクションメソッド(`BeginTrans` / `CommitTrans` / `Rollback`)を用いて自作するのが、今回紹介する「自作BatchUpdate」の神髄である。
—
堅牢かつ爆速な一括更新アーキテクチャ(プロダクションコード)
実際の業務アプリケーションでそのまま組み込める、エラーハンドリング完備のモジュールを提示する。
単に速いだけでなく、「トランザクション途中の予期せぬエラーでデータベースを破損させない」ための堅牢性を担保している。
Option Compare Database
Option Explicit
‘ ==============================================================================
‘ 担当者必携:DAOトランザクション制御による一括高速更新(BatchUpdate)サンプル
‘ ==============================================================================
Public Sub ExecuteBatchUpdateOptimized()
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim lngCounter As Long
Dim startTime As Double
startTime = Timer
‘ 1. ワークスペースとデータベースの参照を取得
‘ ※CurrentDbを都度呼び出すのはオーバーヘッドなので変数に保持する
Set db = CurrentDb
‘ 2. パフォーマンスに優れるダイナナセット(またはテーブルタイプ)でオープン
‘ 大量データ処理ではロック競合を防ぐため必要最小限の範囲を絞ることを推奨
Set rs = db.OpenRecordset(“SELECT ID, Status, ProcessDate FROM T_LargeData WHERE Status = ‘Unprocessed'”, dbOpenDynaset)
If rs.EOF Then
MsgBox “処理対象のレコードが存在しません。”, vbInformation, “情報”
GoTo Cleanup
End If
‘ ==========================================================================
‘ 【重要】トランザクションの開始
‘ ここからCommitTransまたはRollbackまでの全操作が「不可分(アトミック)」になる
‘ ==========================================================================
DBEngine.BeginTrans
On Error GoTo ErrorHandler
lngCounter = 0
With rs
Do While Not .EOF
.Edit
!Status = “Processed”
!ProcessDate = Now
.Update
lngCounter = lngCounter + 1
.MoveNext
Loop
End With
‘ ==========================================================================
‘ トランザクションの確定(Commit)
‘ ここで初めてメモリ上の変更が一括して物理層に書き込まれる
‘ ==========================================================================
DBEngine.CommitTrans
MsgBox “一括更新が完了しました。” & vbCrLf & _
“処理件数: ” & lngCounter & ” 件” & vbCrLf & _
“処理時間: ” & Format(Timer – TimerStart, “0.00秒”), vbInformation, “高速化完了”
Cleanup:
‘ リソースの確実な解放(メモリリークの根絶)
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
Exit Sub
ErrorHandler:
‘ ==========================================================================
‘ 異常系:ロールバックによるデータの整合性維持
‘ ==========================================================================
DBEngine.Rollback
MsgBox “エラーが発生したため、変更をすべてロールバックしました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical, “致命的エラー”
Resume Cleanup
End Sub
—
アーキテクトが教える、現場で活きる実装の急所
上記のコードをプロジェクトに導入する際、シニアエンジニアとして知っておくべき「裏の仕様」と注意点を解説する。
1. `DBEngine.BeginTrans` と `CurrentDb.BeginTrans` の違い
Access VBAでは、トランザクションを開始する方法が複数あるように見える。
- `DBEngine.BeginTrans` (グローバルワークスペース)
- `CurrentDb.BeginTrans` (現在のデータベースインスタンス)
結論として、常に `DBEngine.BeginTrans` を使うべきである。
`CurrentDb` は呼び出すたびに新しいデータベースオブジェクトのインスタンスを生成する。そのため、変数に格納せずにトランザクションを操作すると、インスタンスの不整合を引き起こし、意図通りにロールバックが機能しないバグの温床となる。`DBEngine` を明示することで、DAOのデフォルトワークスペース(`Workspaces(0)`)のトランザクションを確実に制御できる。
2. レコードロックの粒度と排他制御
トランザクションを長時間保持するということは、その間レコードやテーブルがロックされる(あるいはロック領域が拡大する)ことを意味する。
多人数で同時利用するバックエンドデータベース(共有環境)において、数百万件をひとつのトランザクションで囲むと、他のユーザーの操作が完全にフリーズする「ロック競合」を引き起こす。
- 対策: 大規模データの場合は、ループ内で適度な件数(例: 5,000件ごと)にトランザクションを分割・コミットする「チャンク処理(Chunking)」の設計を検討すること。
3. オブジェクトのライフサイクル管理
`CurrentDb` や `Recordset` を開きっぱなしにするコードは、Access特有の「 bloat(ファイルの肥大化)」やメモリリークを引き起こす。
必ず `Cleanup:` ラベルを用意し、`Set rs = Nothing` と `Set db = Nothing` を通る構造にすること。例外発生時でも確実にリソースが解放されるこのイディオムは、プロダクションコードの最低限の免罪符である。
—
最後に:遅いAccess VBAとは決別せよ
「Accessだから遅い」のではない。「アクセスの仕様を理解せずに非効率なコードを書いている」から遅いのだ。
今回紹介したトランザクション制御による一括更新(BatchUpdate)の自作手法を取り入れれば、処理速度は数十倍から数百倍へと跳ね上がる。現場のユーザーを待たせるストレスフルなシステムから脱却し、ロジカルで美しい高速なアプリケーションをあなたの手で構築してほしい。
