【プロ】DAOオブジェクトのメモリリークを徹底排除する、テーブル定義操作のクリーンアップ作法
長年、大規模なAccess VBAシステムを運用してきたエンジニアであれば、一度は「なぜかバッチ処理が徐々に重くなり、最終的にリソース不足でクラッシュする」という悪夢に直面したことがあるはずだ。
タスクマネージャーを覗けば、`MSACCESS.EXE` のメモリ使用量が右肩上がりに膨れ上がっている。再起動すれば何事もなかったかのように動くが、それでは「24時間365日稼働すべき基幹系システム」とは呼べない。
このメモリリークの元凶の多くは、DAO(Data Access Objects)を用いたテーブル定義(TableDef)やリレーションシップの動的制御における、不完全なオブジェクト解放(クリーンアップ作法)にある。
今回は、Accessの内部構造とCOM(Component Object Model)のライフサイクルを知り尽くしたアーキテクトの視点から、DAOリソースを完璧に管理し、メモリリークを根絶するための極意を解説する。
—
1. なぜDAOのテーブル定義操作でメモリリークが起きるのか?
VBAはガベージコレクション(GC)言語ではない。内部的にはCOMの参照カウント方式(Reference Counting)によってメモリ管理が行われている。
特に `TableDef` や `Field`、`Index`、`Relation` といったDAOのコレクションオブジェクトを操作する際、次のようなコードを書いていないだろうか?
‘ 【アンチパターン】これでは確実にメモリリークする
Sub BadTableDefinition()
Dim db As DAO.Database
Dim tdf As DAO.TableDef
Set db = CurrentDb
Set tdf = db.TableDefs.Create(“TempTable”)
tdf.Fields.Append tdf.CreateField(“ID”, dbLong)
db.TableDefs.Append tdf
‘ 処理終了… 変数のスコープアウト任せ
End Sub
このコードの何が問題か。
1. 暗黙のインスタンス化と参照の残留: `CurrentDb` は呼び出すたびに新しいDatabaseオブジェクトのインスタンスを生成し、返却する。これを変数に保持して解放しないだけで、Accessの内部エンジン(ACE)上にセッションが残り続ける。
2. コレクションへの追加とポインタ: `TableDefs.Append` や `Fields.Append` を行うと、オブジェクトの所有権がJet/ACEデータベースエンジン側に移譲される。VBA側で変数(`tdf` 等)を `Set tdf = Nothing` するだけでは、エンジン側の参照が切れないケースが生じ、COMの参照カウンタが0にならない。
結果として、数千回・数万回のループを伴うバッチ処理では、解放されないオブジェクトの残骸がヒープ領域を食いつぶし、メモリリークを引き起こすのだ。
—
2. DAOリソース解放の鉄則:逆順破棄と明示的 `Nothing`
DAOオブジェクトを安全に解放するためには、以下の3つの鉄則を厳守しなければならない。
1. 取得したオブジェクトは、生成と逆の順序(LIFO)で `Nothing` を代入する
- 例: `Database` → `TableDef` → `Field` の順で取得した場合、`Field` → `TableDef` → `Database` の順で解放する。
2. `CurrentDb` は変数に格納して使い回し、必ず明示的に閉じる・解放する
- ループ内で `CurrentDb` を直接叩くのは御法度である。
3. エラーハンドリング(`On Error GoTo`)内でも確実にクリーンアップを通す
—
3. 【実践】極限まで最適化されたテーブル定義・操作テンプレート
以下に、実務のバッチ処理でそのまま使える、メモリリークフリーのテーブル定義・インデックス追加の模範コードを示す。
Option Compare Database
Option Explicit
”’
”’
Public Sub ProCreateOptimizedTable()
Dim db As DAO.Database
Dim tdf As DAO.TableDef
Dim fldID As DAO.Field
Dim fldName As DAO.Field
Dim idx As DAO.Index
Dim isTableCreated As Boolean
isTableCreated = False
‘ エラーハンドリングの準備
On Error GoTo ErrorHandler
‘ 1. Databaseオブジェクトの取得(CurrentDbは変数に保持)
Set db = CurrentDb
‘ 既存の同名テーブルが存在する場合は削除する(安全な存在チェック)
If TableExists(db, “M_DataStore”) Then
db.TableDefs.Delete “M_DataStore”
db.TableDefs.Refresh
End If
‘ 2. TableDefオブジェクトの生成
Set tdf = db.TableDefs.Create(“M_DataStore”)
‘ 3. フィールドの作成と追加
Set fldID = tdf.CreateField(“RecordID”, dbLong)
fldID.Attributes = dbAutoIncrField ‘ 自動採番
tdf.Fields.Append fldID
Set fldName = tdf.CreateField(“DataValue”, dbText, 255)
fldName.Required = True
tdf.Fields.Append fldName
‘ 4. インデックスの追加(プライマリキー)
Set idx = tdf.CreateIndex(“PrimaryKey”)
idx.Fields.Append idx.CreateField(“RecordID”)
idx.Primary = True
idx.Unique = True
tdf.Indices.Append idx
‘ 5. テーブル定義をデータベースに登録(ここでエンジン側にコミットされる)
db.TableDefs.Append tdf
isTableCreated = True
MsgBox “テーブルの作成が正常に完了しました。”, vbInformation
CleanUp:
‘ ==========================================
‘ 徹底的なクリーンアップ(生成と逆順で解放)
‘ ==========================================
‘ 個別オブジェクトの参照を解放
If Not idx Is Nothing Then Set idx = Nothing
If Not fldName Is Nothing Then Set fldName = Nothing
If Not fldID Is Nothing Then Set fldID = Nothing
If Not tdf Is Nothing Then Set tdf = Nothing
‘ 最後にDatabaseを解放
If Not db Is Nothing Then
‘ 必要に応じて db.Close を検討(CurrentDbの場合は通常不要だが、
‘ DBEngine(0)(0) や DAO.DBEngine.OpenDatabase を使った場合は必須)
Set db = Nothing
End If
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
‘ エラー時も確実にリソースを回収する
Resume CleanUp
End Sub
”’
”’
Private Function TableExists(ByRef targetDb As DAO.Database, ByVal tableName As String) As Boolean
Dim t As DAO.TableDef
Dim exists As Boolean
exists = False
‘ 走査時はエラーを抑制しつつチェック
On Error Resume Next
Set t = targetDb.TableDefs(tableName)
If Err.Number = 0 Then exists = True
On Error GoTo 0
Set t = Nothing
TableExists = exists
End Function
—
4. チーフアーキテクトからの高度な知見:APIとメモリ最適化の極み
ここまでの対策で通常のシステムは安定するが、さらにシビアな「数百万件のレコードを扱うシステム間連携バッチ」や「レガシー環境の極限チューニング」においては、以下のアーキテクチャ的アプローチが不可欠となる。
1. `CurrentDb` と `DBEngine(0)(0)` の使い分け
- `CurrentDb()`: 呼び出すたびにキャッシュをバイパスして新しいインスタンスを生成するため、ループ内で呼び出すとパフォーマンスが低下し、リークの温床になる。必ず冒頭で1回取得して変数に代入すること。
- `DBEngine(0)(0)`: 現在のワークスペースのデフォルトデータベースを指す。こちらはインスタンスがキャッシュされるため高速だが、マルチスレッド環境や複雑なトランザクション制御では挙動に注意が必要である。
2. 強制ガベージコレクションの誘発(Windows APIの活用)
VBAから直接COMの解放タイミングを完全に制御することはできないが、重い処理のイテレーションの節目でメモリの断片化を解消するために、Windows API(`SetProcessWorkingSetSize`)を呼び出してワーキングセットをトリミングする手法が、レガシー現場の最終防衛ラインとして使われることがある。
‘ Windows APIの宣言
If VBA7 Then
Private Declare PtrSafe Function SetProcessWorkingSetSize Lib “kernel32” ( _
ByVal hProcess As LongPtr, _
ByVal dwMinimumWorkingSetSize As LongPtr, _
ByVal dwMaximumWorkingSetSize As LongPtr) As Long
Private Declare PtrSafe Function GetCurrentProcess Lib “kernel32” () As LongPtr
Else
Private Declare Function SetProcessWorkingSetSize Lib “kernel32” ( _
ByVal hProcess As Long, _
ByVal dwMinimumWorkingSetSize As Long, _
ByVal dwMaximumWorkingSetSize As Long) As Long
Private Declare Function GetCurrentProcess Lib “kernel32″ () As Long
End If
”’
”’ ※多用は禁物だが、巨大バッチの終了時に有効
”’
Public Sub TrimMemoryFootprint()
Dim hProc As LongPtr
hProc = GetCurrentProcess()
‘ ワーキングセットの最小/最大を一時的に絞ることで、OS側へメモリを返還させる
Call SetProcessWorkingSetSize(hProc, -1&, -1&)
End Sub
※注意: このAPIはOSにメモリの解放を強制するものであり、根本的なメモリリークの「解決」にはならない。あくまで「リークさせないコードを書いた上での、パフォーマンストーンの補助」として扱うべきである。
—
5. まとめ
Access VBAにおけるDAO操作は、手軽であるゆえにメモリ管理が疎かになりやすい。しかし、プロのシステムエンジニアが構築するアーキテクチャにおいて、「動けばいい」という妥協は許されない。
- オブジェクトの生成と破棄は必ず対にし、逆順(LIFO)で `Nothing` を代入する。
- `CurrentDb` の乱用を避け、スコープを意識した変数管理を徹底する。
- エラーパスを通った場合でも確実にクリーンアップされる構造(`GoTo CleanUp`)を義務付ける。
この作法をチーム全体で徹底し、レガシーの呪縛から解放された、真に堅牢なAccessデータベースシステムを築き上げてほしい。
