【実務・中級編】CurrentDbとDBEngine(0)(0)のメモリ管理:DAO接続を最適化する設計パターン – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:CurrentDbとDBEngine(0)(0)のメモリ管理──DAO接続を最適化する設計パターン

開発現場でこんなコードを見かけるたび、私はエンジニアとしての危機感を覚える。

‘ 【アンチパターン】ループ内でCurrentDbを叩く愚行
Dim i As Long
For i = 1 to 10000
CurrentDb.Execute “UPDATE T_Stock SET Stock = Stock – 1 WHERE ID = ” & i, dbFailOnError
Next i

動く。確かに動く。しかし、このコードはAccessの心臓部であるDAO(Data Access Objects)エンジンに対して、数千回もの「データベースセッションの生成と破壊」を強制している。

Access VBAで大規模な業務システムや堅牢なツールを構築する時、パフォーマンスのボトルネック、そして原因不明の「メモリリーク」「リソース不足エラー」の多くは、このデータベースオブジェクトのライフサイクル管理の欠如に起因する。

今回は、`CurrentDb`と`DBEngine(0)(0)`の挙動の本質を解き明かし、実務で絶対に破綻しないDAO接続の最適化設計パターンを伝授する。

1. なぜ `CurrentDb` は遅いのか?(オブジェクトモデルの裏側)

多くの開発者は、`CurrentDb`を「現在のデータベースを返す便利な関数」程度に認識している。だが、アーキテクトの視点は違う。

CurrentDbの正体

`CurrentDb`メソッドは、呼び出されるたびに新しいDAOのDatabaseオブジェクトをメモリ上に「新規生成(インスタンス化)」し、それを返す。
つまり、ループ内で呼び出すということは、数千回も重たいデータベースのセッションを開閉しているのと同義である。これではCPUもメモリも悲鳴を上げる。

DBEngine(0)(0) との決定的な違い

一方で、`DBEngine(0)(0)`(または `DBEngine.Workspaces(0).Databases(0)`)はどうか。
これは、現在Accessのユーザーインターフェースが開いているメインのデータベースへの永続的な参照(セッション)を返す。すでにメモリ上に存在するインスタンスを直接指すため、`CurrentDb`のような生成コストが発生しない。

では、「常に `DBEngine(0)(0)` を使えば速いのか?」と言えば、答えはNOだ。ここにAccess VBA最大の罠がある。

2. 致命的な罠:DBEngine(0)(0) のキャッシュ問題

`DBEngine(0)(0)` は高速だが、大きな欠点がある。それは「Accessの内部キャッシュを保持し続ける」という点だ。

例えば、VBAからテーブル構造を変更したり(DDL実行)、外部からデータが更新されたりしたとき、`DBEngine(0)(0)` は古いキャッシュ(スキーマやレコードセットの状態)を掴み続けることがある。結果として、「コードを実行したのに最新のデータが反映されない」「幽霊レコードが見える」といった、デバッグ泣かせの怪奇現象を引き起こす。

黄金律:使い分けの原則

  • `CurrentDb`:

構造変更(DDL)、トランザクション制御、あるいは安全性が最優先される単発のクエリ実行に使う。毎回フレッシュな状態を保証する。

  • `DBEngine(0)(0)`:

同一セッション内で数千・数万回のレコード操作(DML)を行うなど、極限のパフォーマンスが求められるループ処理の内部で使う。ただし、使い終わったら適切に参照を解放する。

3. 【実践】プロダクションコード:DAO接続を極限まで最適化する設計パターン

現場でそのまま使える、堅牢性とスピードを両立させたクラス・モジュールレベルの設計パターンを提示する。

ここでは、トランザクションとエラーハンドリングを完備し、`DBEngine(0)(0)` を安全に使い倒すラッパー的なアプローチをとる。

実装コード例

Option Compare Database
Option Explicit

‘ =================================================================
‘ 模块名: M_DataService
‘ 概要 : DAO接続のライフサイクルを管理し、極限のパフォーマンスを発揮するデータ操作モジュール
‘ =================================================================

Public Sub ExecuteBulkUpdateSample()
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim startTime As Double
startTime = Timer

‘ 【設計のポイント 1】
‘ 高速化が必要な大量処理の入口で一度だけ参照を取得する。
‘ これにより、ループ内のオーバーヘッドを完全に排除する。
Set db = DBEngine(0)(0)

‘ トランザクション開始(パフォーマンス向上と整合性担保の鉄則)
db.BeginTrans

On Error GoTo ErrorHandler

‘ 例としてパラメータクエリやレコードセット操作を行う場合
Set rs = db.OpenRecordset(“T_PerformanceTest”, dbOpenDynaset)

Dim counter As Long
counter = 0

Do Until rs.EOF
rs.Edit
‘ 値の更新処理
rs!ProcessedFlag = True
rs!UpdateDate = Now()
rs.Update

counter = counter + 1
rs.MoveNext
Loop

‘ コミット
db.CommitTrans

Debug.Print “処理成功: ” & counter & “件 / 実行時間: ” & (Timer – startTime) & “秒”

CleanUp:
‘ 【設計のポイント 2】
‘ オブジェクトの明示的な解放。メモリリークを防ぐための防壁。
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
‘ 注意: DBEngine(0)(0) 自体はグローバルなセッションのため、
‘ Set db = Nothing は行うが、Access終了時まで実体は維持される。
Set db = Nothing
Exit Sub

ErrorHandler:
‘ 異常系:ロールバックによるデータの堅牢性担保
db.Rollback
MsgBox “エラーが発生しました: ” & Err.Description, vbCritical, “致命的エラー”
Resume CleanUp
End Sub

4. チーフアーキテクトからの提言:保守性を損なわないために

コードの最適化は重要だが、可読性や保守性を犠牲にしてはならない。

1. 安易なグローバル化の禁止
`DBEngine(0)(0)` をあちこちの標準モジュールで勝手に取得して解放し忘れると、メモリリークの温床になる。データアクセスを行うレイヤー(クラスモジュールや専用のデータアクセスメソッド)を明確に分離し、そこでライフサイクルを完結させよ。
2. 必ず `dbFailOnError` を付与せよ
`CurrentDb.Execute` や `db.Execute` を使う際は、必ず第2引数に `dbFailOnError` を指定すること。これを怠ると、途中でエラーが起きてもデータベースがサイレントに部分更新(ダーティな状態)を行い、データが破損する。プロとして絶対に許されないミスだ。
3. トランザクションのスコープを最小限に
`BeginTrans` から `CommitTrans` までの範囲は、極力短く保て。長大なトランザクションはロック競合を引き起こし、マルチユーザー環境のAccessシステムを瞬時にクラッシュさせる。

最後に

Access VBAは「おもちゃの言語」ではない。その裏側でうごめくDAO/Jetエンジンの挙動を正しく理解し、メモリのライフサイクルを支配できた時、Accessは極めて堅牢で高速な業務システム基盤へと変貌する。

「動けばいい」の時代は終わった。
プロフェッショナルとして、背筋の通った美しいコードを書き続けよう。

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