【テクニカル・上級編】【上級】Application.CurrentDbをキャッシュすべき理由:DAO.Database変数の再利用によるパフォーマンス最適化 – Access VBA解析バイブル

スポンサーリンク

【上級】Application.CurrentDbをキャッシュすべき理由:DAO.Database変数の再利用によるパフォーマンス最適化

レガシーシステムの最前線でAccess VBAと向き合ってきたエンジニアならば、一度はパフォーマンスの壁に直面したことがあるはずだ数千件のレコードをループ処理で更新する際、なぜか処理が徐々に重くなる、あるいはCPU使用率が跳ね上がる現象だ。

その元凶の多くは、コードのあちこちに散らばる無防備な `CurrentDb` の呼び出しにある。

今回は、Accessオブジェクトモデルの深層に踏み込み、`Application.CurrentDb` が抱える構造的欠陥と、DAO.Database変数のキャッシュ(再利用)による極限のパフォーマンス最適化について、アーキテクトの視点から解説する。

1. なぜ `CurrentDb` の多用は悪なのか?(オブジェクト生成のメカニズム)

多くの開発者は、次のように記述する。

Dim i As Long
For i = 1 to 10000
CurrentDb.Execute “UPDATE T_Stock SET Quantity = Quantity – 1 WHERE ItemID = ” & i, dbFailOnError
Next i

一見、何の問題もないように見えるこのコード。しかし、裏側で何が起きているかを理解しているだろうか。

`CurrentDb` の正体:毎回のポインタ生成とスキーマキャッシュの破棄

`Application.CurrentDb` は、プロパティではなくメソッドである。これを呼び出すたびに、Accessの内部エンジン(ACE/Jet)は以下の重い処理を強制される。

1. 現在開いているデータベースへの新しい `DAO.Database` オブジェクトのインスタンス をメモリ上に生成する。
2. システムテーブル(MSysObjectsなど)にアクセスし、データベースのスキーマ情報を再評価・再構築する。
3. 処理がスコープを抜けた際、あるいはオブジェクトが破棄される際に、メモリ解放とロックの解除を行う。

これを1万回繰り返すということは、1万回データベースへの新規接続とスキーマの再ロードを行っているに等しい。これではパフォーマンスが劣化して当然だ。

2. `CurrentDb` と `DBEngine.Workspaces(0).Databases(0)` の決定的な違い

ここで、Access VBAにおけるデータベース参照のもう一つの手段である `DBEngine` について触れておこう。

  • `CurrentDb`: 呼び出すたびに新しい `DAO.Database` インスタンスを返す。
  • `DBEngine.Workspaces(0).Databases(0)` (通称 `CodeDb`): 現在実行中のコードが存在するデータベースの既存のインスタンスを返す(すでに開かれていればキャッシュされたものを返す)。

「じゃあ `CodeDb` を使えばいいのか?」という短絡的な結論に飛びついてはならない。
`CodeDb` はアドインやライブラリデータベース(.accda)の文脈では意図した挙動をしない場合があり、またマルチユーザー環境やトランザクション制御の観点から、明示的なインスタンス管理を行う方が圧倒的に安全かつ堅牢である。

シニアエンジニアが取るべきアプローチは、「自分で `DAO.Database` 変数をインスタンス化し、スコープを統制して使い回す(キャッシュする)」 ことだ。

3. 実装パターン:DAO.Database変数のモジュールレベル・キャッシュ

パフォーマンスを極限まで高めるための設計パターンの核心を示す。
ポイントは、データベース接続のライフサイクルをプロシージャ単位ではなく、クラスあるいは標準モジュールのライフサイクルに同期させることだ。

以下のコードは、トランザクションの整合性を保ちつつ、インスタンスの生成コストをゼロにする最適化されたパターンである。

‘ =========================================================================
‘ 標準モジュール: mOptimizedDataAccess
‘ =========================================================================
Option Explicit

‘ モジュールレベルでDAO.Database変数を保持(これがキャッシュの本体)
Private m_dbCached As DAO.Database


‘ キャッシュされたDatabaseオブジェクトを取得する(なければ生成する)
‘ @return DAO.Database
‘ =========================================================================
Public Function GetCachedDb() As DAO.Database
‘ オブジェクトが未初期化、またはメモリから解放されている場合のみ再取得
If m_dbCached Is Nothing Then
Set m_dbCached = Application.CurrentDb
Else
‘ 接続が有効か確認するためのダミーアクセス(必要に応じてエラーハンドリング)
On Error Resume Next
If m_dbCached.Transactions Then ‘ プロパティ参照で生存確認
‘ 生きている
End If
If Err.Number <> 0 Then
‘ 意図せず切断されている場合は再取得
Set m_dbCached = Application.CurrentDb
End If
On Error GoTo 0
End If

Set GetCachedDb = m_dbCached
End Function


‘ アプリケーション終了時やセッション終了時に必ず呼び出すこと
‘ =========================================================================
Public Sub ReleaseCachedDb()
If Not m_dbCached Is Nothing Then
Set m_dbCached = Nothing
End If
End Sub


‘ 大量データ処理の実行例
‘ =========================================================================
Public Sub ExecuteBulkUpdateSample()
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim startTime As Double

startTime = Timer

‘ キャッシュからデータベース参照を取得(オーバーヘッドは初回のみ)
Set db = GetCachedDb()

‘ トランザクションの開始(DAOによる高速化の真骨頂)
db.BeginTrans
On Error GoTo TransError

‘ 事前にQueryDefをコンパイルして使い回す(さらにパフォーマンス向上)
‘ ※パラメータクエリを使うことでSQLインジェクション対策と解析コスト削減を両立
Set qdf = db.CreateQueryDef(“”, “UPDATE T_Stock SET Quantity = Quantity – 1 WHERE ItemID = [p1]”)

Dim i As Long
For i = 1 to 10000
qdf.Parameters(“p1”).Value = i
qdf.Execute dbFailOnError
Next i

db.CommitTrans
Debug.Print “処理完了時間: ” & (Timer – startTime) & ” 秒”

‘ クリーンアップ(qdfは一時クエリなので即座に解放)
qdf.Close
Set qdf = Nothing
Exit Sub

TransError:
db.Rollback
MsgBox “エラー発生: ” & Err.Description, vbCritical
If Not qdf Is Nothing Then
qdf.Close
Set qdf = Nothing
End If
End Sub

4. メモリ最適化とオブジェクトのライフサイクル管理

VBAにおける最大の罠は、「ガベージコレクションの挙動が曖昧である」 という点に尽きる。
特にAccessのCOMコンポーネントモデルにおいて、参照カウントが残ったままオブジェクトが放置されると、メモリリークを引き起こし、最悪の場合Accessプロセスそのものが不安定になる。

キャッシュ戦略における鉄則

1. 開放の責任を明確にする
モジュールレベルの `m_dbCached` は便利だが、アプリケーションが終了するまでメモリ上に居座る。長期間起動し続ける常駐型システムでは、フォームの閉じるイベントや、特定のバッチ処理の終了時に必ず `ReleaseCachedDb` を明示的に呼び出し、`Set m_dbCached = Nothing` を実行して参照を断ち切らなければならない。

2. レコードセット(Recordset)との混同を避ける
`DAO.Database` のキャッシュは有効だが、`DAO.Recordset` や `DAO.QueryDef` をモジュールレベルでキャッシュすることは厳禁である。これらはデータフェッチの状態やカーソルを保持するため、使い回すと予期せぬデータの競合や「Crsor is not open」といった致命的なランタイムエラーを引き起こす。キャッシュしていいのはあくまで `Database` インスタンス(接続)のみである。

5. レガシー環境・システム間連携への応用

この「データベース接続のキャッシュ」という概念は、Access単体の処理に留まらない。
例えば、ADODBを用いたSQL ServerやOracleなどの外部データベース連携、あるいはWindows API(COMオブジェクト)を介したExcel操作との混成環境においても、「外部リソースへのアクセスハンドルをむやみに破棄・生成しない」 という原則は共通の最高最適化プラットフォームである。

レガシーなAccessシステムが「遅い」「フリーズする」と嘆かれる原因の多くは、ハードウェアの性能不足ではなく、ソフトウェア設計における無駄なインスタンス生成の積み重ねにある。

`Application.CurrentDb` の挙動を熟知し、DAO.Database変数を適切にコントロールすること。それこそが、時代遅れと言われがちなAccess VBAから極限のパフォーマンスを引き出す、我々シニアエンジニアの技術的特権なのだ。

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