【実務・中級編】【プロ】DAOオブジェクトのメモリリークを徹底排除する、テーブル定義操作のクリーンアップ作法 – Access VBA解析バイブル

スポンサーリンク

【プロ】DAOオブジェクトのメモリリークを徹底排除する、テーブル定義操作のクリーンアップ作法

Access VBAでの開発において、テーブルの動的生成、フィールドの追加、リレーションシップの構築といった「TableDef」や「Relation」を絡めたメタデータ操作は、大規模な業務自動化バッチにおいて不可欠なアプローチだ。

しかし、現場のコードを見渡すと、「動的なテーブル定義変更を行った途端、処理を重ねるごとにメモリ消費量が増大し、最終的にAccessがフリーズする・落ちる」という致命的なトラブルに直面しているエンジニアが後を絶たない。

原因は明白だ。DAO(Data Access Objects)のオブジェクト階層と、VBAの背後にあるCOMコンポーネントのライフサイクルを正しく理解せず、参照の解放(クリーンアップ)を怠っていることにある。

今回は、数日間に及ぶ長時間のバッチ処理や、数千回におよぶテーブル構築・削除のループを回しても「1バイトのメモリリークすら起こさない」、プロフェッショナルなリソース管理の極意を伝授しよう。

—

なぜDAOの操作はメモリリークを起こしやすいのか?

1. COMオブジェクトの参照カウンタとVBAのガベージコレクションの限界

VBAは簡易的なガベージコレクションを備えているが、DAOやADOといったCOMオブジェクトの背後にあるC++ネイティブのリソースまでは完璧に管理しきれない。
特に `CurrentDb.TableDefs` のようなメソッドチェーンや、変数に代入したオブジェクトの参照は、明示的に解放(`Set obj = Nothing`)しない限り、VBAの実行環境(プロセス)に残り続ける。

2. 「見えない参照」のトラップ

次のようなコードを書いたことはないだろうか?

‘ 【悪例】絶対にやってはいけない書き方
Sub BadExample()
Dim tdf As TableDef
Set tdf = CurrentDb.CreateTableDef(“Tmp_Table”)
tdf.Fields.Append tdf.CreateField(“ID”, dbLong)
CurrentDb.TableDefs.Append tdf
‘ 処理終了… tdf を解放していない!
End Sub

このコードの何が問題か?
1. `CurrentDb` は呼び出すたびに新しい一時的なDatabaseオブジェクトのインスタンスを生成している。メソッドチェーンやプロパティの多用により、参照が宙に浮いたまま回収されなくなる。
2. `TableDef` や `Field` オブジェクトがメモリ上に保持されたままになり、ループ内でこれを繰り返すと、ヒープ領域が確実に枯渇する。

—

メモリリークを完全封鎖する3大鉄則

実務のプロダクションコードにおいて、DAOを扱う際は以下の3つを鉄則とする。

1. Databaseオブジェクトは必ず変数に格納し、使い回す(`CurrentDb`の乱用禁止)
2. 生成したオブジェクト(`TableDef`, `Field`, `Relation`, `Recordset`等)は、下位階層から順番に `Set xxx = Nothing` で明示的に解放する
3. 予期せぬエラー(実行時エラー)が発生しても必ずクリーンアップを通るよう、`On Error GoTo` による確実な経路制御を行う

—

【コピペOK】堅牢なプロダクションコード例

以下に、動的にテーブルを生成し、フィールドとインデックス、リレーションシップを構築した上で、完全にメモリを解放する実務仕様のプロシージャを示す。

保守性を高めるため、エラーハンドリングとクリーンアップブロックを完全に分離した設計にしている。

Option Compare Database
Option Explicit

‘ =========================================================================
‘ 処理名 : CreateDynamicTableSafely
‘ 概要 : DAOを使用した安全なテーブル定義・リレーション構築のサンプル
‘ 備考 : メモリリークを完全に防ぐためのクリーンアップ作法を実装
‘ =========================================================================
Public Sub CreateDynamicTableSafely()
Dim db As DAO.Database
Dim tdf As DAO.TableDef
Dim fld As DAO.Field
Dim idx As DAO.Index
Dim rel As DAO.Relation

Dim tableName As String
tableName = “M_SecureMaster”

‘ エラーハンドラーの設定
On Error GoTo ErrorHandler

‘ 1. Databaseオブジェクトを変数に取得(CurrentDbの乱用を防ぐ)
Set db = CurrentDb

‘ — 【事前準備】同名テーブルが存在する場合は削除 —
If TableExists(db, tableName) Then
db.TableDefs.Delete tableName
db.TableDefs.Refresh
End If

‘ 2. TableDefオブジェクトの生成
Set tdf = db.CreateTableDef(tableName)

‘ 3. フィールドの追加
‘ IDフィールド(長整数型、主キー)
Set fld = tdf.CreateField(“ID”, dbLong)
fld.Attributes = fld.Attributes Or dbAutoIncrField ‘ 自動増加
tdf.Fields.Append fld
Set fld = Nothing ‘ 個別解放

‘ Codeフィールド(テキスト型)
Set fld = tdf.CreateField(“Code”, dbText, 50)
fld.Required = True
tdf.Fields.Append fld
Set fld = Nothing ‘ 個別解放

‘ 4. テーブルの追加(TableDefsコレクションへの登録)
db.TableDefs.Append tdf
db.TableDefs.Refresh

‘ 5. インデックスの追加(主キーの設定)
Set idx = tdf.CreateIndex(“PrimaryKey”)
idx.Fields.Append idx.CreateField(“ID”)
idx.Primary = True
idx.Unique = True
tdf.Indexes.Append idx
tdf.Indexes.Refresh
Set idx = Nothing ‘ 個別解放

‘ 6. リレーションシップの構築例(既存の親テーブルが存在すると仮定)
‘ ※親テーブル “M_Parent” の “ID” と外部キーを結ぶ場合
‘ On Error Resume Next 等で既存リレーションの削除処理を入れておくとさらに堅牢
Set rel = db.CreateRelation(“FK_Parent_SecureMaster”, “M_Parent”, tableName, dbRelationUpdateCascade)
Set fld = rel.CreateField(“ID”)
fld.ForeignField = “ID”
rel.Fields.Append fld
db.Relations.Append rel
db.Relations.Refresh

MsgBox “テーブル定義とリレーションの構築が正常に完了しました。”, vbInformation, “成功”

CleanUp:
‘ =========================================================================
‘ 【極意】下位オブジェクトから順に、確実に参照を破棄する
‘ =========================================================================
If Not fld Is Nothing Then Set fld = Nothing
If Not rel Is Nothing Then Set rel = Nothing
If Not idx Is Nothing Then Set idx = Nothing
If Not tdf Is Nothing Then Set tdf = Nothing
If Not db Is Nothing Then Set db = Nothing
Exit Sub

ErrorHandler:
MsgBox “エラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical, “致命的なエラー”

‘ エラー時も必ずクリーンアップを通す
Resume CleanUp
End Sub

‘ ————————————————————————-
‘ 補助関数: 指定したテーブルが存在するか判定する
‘ ————————————————————————-
Private Function TableExists(db As DAO.Database, ByVal targetName As String) As Boolean
Dim tdf As DAO.TableDef
Dim exists As Boolean

exists = False
For Each tdf In db.TableDefs
If tdf.Name = targetName Then
exists = True
Exit For
End If
Next tdf

TableExists = exists

‘ ループ変数も解放を意識する(For Eachのtdfは自動解放されるが、念のため)
Set tdf = Nothing
End Function

—

プロジェクトリーダーからの実践的アドバイス

ループ処理内でテーブル定義を変更する場合の注意点

もし、数百件のテーブルを動的に生成・削除するようなバッチ処理を書く場合、上記のコードであっても、ループのスコープ内に `db` や `tdf` を閉じ込め、1周ごとに確実に `Set … = Nothing` を実行することが絶対条件となる。

また、DAOを使った大量のスキーマ変更はAccessの内部データベースファイル(ACCDB)にフラグメント(断片化)を引き起こす。バッチの最後には必ず `CompactDatabase`(データベースの最適化)を実行する設計を組み込んでおくべきだ。これが、稼働し続けるシステムと、数ヶ月で壊れるシステムの分水嶺となる。

小手先のテクニックではなく、オブジェクトの寿命(ライフサイクル)を支配する者だけが、Accessを真の基幹システムへと昇華させることができる。現場のコードを今すぐ見直し、メモリリークの芽を断ち切ってほしい。

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