こんにちは!Access VBAの世界へようこそ。
マクロの記録から一歩踏み出し、「自分の手でシステムをコントロールしたい」と願うあなたへ、今日はとてもエキサイティングな技術をお伝えします。
今回取り上げるのは、「QueryDef(クエリ定義)を再帰的に呼び出し、動的SQLで階層データ(ツリー構造)をスマートに抽出する手法」です。
「組織図」や「部品表(BOM)」のように、親と子が枝分かれしていくデータを扱うとき、固定のクエリでは太刀打ちできなくなりますよね。「何階層下まであるか分からないデータをどうやって抽出するのか?」……ここをクリアできれば、あなたのAccess VBAのスキルは確実に中級者から“上のステージ”へとシフトします。
難しい言葉はなるべく使わず、隣で先輩エンジニアが優しくコードを紐解くように解説していきます。さあ、一緒にバッチリ基本をマスターしましょう!
—
1. なぜ「階層データ」と「QueryDef」なのか?
組織図や部品表を想像してみてください。「A部長の下にB課長がいて、B課長のの下にC主任がいて……」という構造ですね。これをデータベースで表現すると、同じテーブルの中で「自分の親は誰か(親ID)」を持たせる形になります。
これを普通のSQL一発で何階層も一網打尽にしようとすると、AccessのSQL(Jet/ACEエンジン)では限界があります。そこで登場するのが、QueryDefの動的生成と「再帰(自分自身を呼び出すこと)」です。
そもそも QueryDef ってなに?
Accessの画面で「クエリデザイン」を作って保存しますよね?あれの裏側の正体が QueryDef です。
VBAからこのQueryDefを「その場で書き換えて、実行して、結果を受け取る」という芸れざを覚えると、Accessのパフォーマンスと表現力は爆発的に跳ね上がります。
—
2. 処理の全体像(イメージ図)
言葉だけだとイメージしにくいので、コードを書く前に頭の中にマップを作りましょう。
[起点となるID (例: 10)]
↓
【QueryDefの動的生成】 “親IDが 10 のレコードをくれ!”
↓
[ヒットした子たち (例: 20, 21)] を取得
↓
┌─────────────────┴─────────────────サイズ可変のループ
▼ ▼
[ID:20 を親として再帰呼び出し] [ID:21 を親として再帰呼び出し]
↓ ↓
(さらにその下の子を探索…) (さらにその下の子を探索…)
このように、「見つかった子たちを次の親にして、同じ処理を繰り返す(再帰)」のがアルゴリズムの核心です。
—
3. 実装コード:動的QueryDefによるツリー抽出
それでは、実際の開発現場でそのままコピペして使えるサンプルコードを見ていきましょう。
今回はわかりやすく「社員の組織ツリー」を想定した `T_Employees` テーブル(ID, 氏名, 部署, 上司ID)があるものとして解説します。
標準モジュールに以下のコードを貼り付けてみてください。
‘ ==============================================================================
‘ テーマ: QueryDefの動的生成による階層データ(ツリー構造)の再帰抽出
‘ 執筆者: チーフアーキテクト(あなたを導く先輩エンジニアより)
‘ ==============================================================================
Option Explicit
‘ 抽出結果を蓄積するためのテンポラリテーブル等を想定したパブリック変数
‘ (実務ではコレクションや別テーブルに吐き出すのが安全です)
Private g_ResultCount As Long
Public Sub StartRecursiveExtraction(ByVal rootId As Long)
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim qdfName As String
Set db = CurrentDb
qdfName = “tmp_DynamicTreeQuery”
‘ 1. すでに一時QueryDefがあれば削除しておく(お掃除はエンジニアの基本です)
On Error Resume Next
db.QueryDefs.Delete qdfName
On Error GoTo 0
‘ 2. 一時的なQueryDefを作成(ここでSQLの枠組みを作っておきます)
Set qdf = db.CreateQueryDef(qdfName)
g_ResultCount = 0
Debug.Print “=== 階層抽出プロセス開始 (起点ID: ” & rootId & “) ===”
‘ 3. 再帰処理のスタート(親IDのリストを渡すイメージ)
Call ExtractChildrenRecursive(db, qdf, rootId, 1)
‘ 4. 後片付け
db.QueryDefs.Delete qdfName
Debug.Print “=== 抽出完了. 総レコード数: ” & g_ResultCount & ” ===”
End Sub
‘ ==============================================================================
‘ 再帰的に呼び出されるコアプロシージャ
‘ ==============================================================================
Private Sub ExtractChildrenRecursive(ByRef db As DAO.Database, ByRef qdf As DAO.QueryDef, ByVal parentId As Long, ByVal currentLevel As Long)
Dim rs As DAO.Recordset
Dim dynamicSql As String
Dim indent As String
‘ ログ用のインデント作成(階層が深くなるにつれてスペースを増やす)
indent = String((currentLevel – 1) 4, ” “)
‘ — A. 動的SQLの構築 —
‘ 引数で受け取った parentId を埋め込んだSQLをその場で生成します
dynamicSql = “SELECT ID, Name, ManagerID ” & _
“FROM T_Employees ” & _
“WHERE ManagerID = ” & parentId & “;”
‘ QueryDefのSQLプロパティを「その場で書き換える」!
qdf.SQL = dynamicSql
‘ — B. クエリの実行とレコードセットの取得 —
Set rs = qdf.OpenRecordset(dbOpenSnapshot)
‘ — C. データの走査とさらなる再帰呼び出し —
Do Until rs.EOF
g_ResultCount = g_ResultCount + 1
‘ 抽出されたデータの確認(イミディエイトウインドウに出力)
Debug.Print indent & “Lv.” & currentLevel & ” -> [ID: ” & rs!ID & “] ” & rs!Name
‘ ★ここがポイント!今見つかったレコードのIDを「次の親」として自分自身を呼ぶ
Dim nextParentId As Long
nextParentId = rs!ID
‘ 再帰呼び出し(階層を +1 する)
Call ExtractChildrenRecursive(db, qdf, nextParentId, currentLevel + 1)
rs.MoveNext
Loop
‘ オブジェクトの解放(メモリリークを防ぐプロの作法)
rs.Close
Set rs = Nothing
End Sub
—
4. コードの深掘り解説:ここがプロの勘所
初心者の方が「おっ」と躓きやすいポイントや、パフォーマンス上の重要事項を3つに絞って解説します。
① `qdf.SQL = dynamicSql` による「動的SQLの書き換え」
通常のクエリは「固定されたSQL」を持ちますが、QueryDefオブジェクトはコードの実行中にSQL文を自由自在に書き換えることができます。
ループの中でわざわざ毎回クエリを作り直さず、最初に作った `qdf` の中身(SQL文)だけをパパッと書き換えて `OpenRecordset` する。これが、Access VBAで高速かつスマートに動的クエリを扱うための極意です。
② メモリの掃除(`rs.Close` と `Set rs = Nothing`)
再帰処理(自分自身を何回も呼び出す処理)は、メモリを大量に消費しやすい特徴があります。
もし `rs`(レコードセット)を閉じ忘れたり、`db.QueryDefs.Delete` をサボったりすると、Accessの内部メモリがパンクし、最悪の場合はアプリケーションが強制終了(フリーズ)します。
「開いたものは必ず閉じる、使い終わった参照は `Nothing` を入れる」。この鉄則を、再帰処理ではいつも以上に厳格に守ってください。
③ インデントによる可視化
コード内の `indent = String((currentLevel – 1) 4, ” “)` は、イミディエイトウィンドウにツリー構造を綺麗に美しく描画するためのテクニックです。
実際に動かしたとき、コンソールに以下のようにきれいに階層が表示されると、デバッグがものすごく楽しくなりますよ!
=== 階層抽出プロセス開始 (起点ID: 1) ===
Lv.1 -> [ID: 2] 佐藤部長
Lv.2 -> [ID: 5] 鈴木課長
Lv.3 -> [ID: 12] 田中主任
Lv.2 -> [ID: 6] 高橋課長
=== 抽出完了. 総レコード数: 4 ===
—
5. 陥りやすいエラーと回避のテクニック
この階層ツリー抽出を実装する際、開発現場でよくある「罠」と、その回避策をシェアしておきます。
- 罠1:無限ループ(循環参照)にはまる
- 原因:データ側のミスで、「Aさんの上司がBさん、Bさんの上司がAさん」のようなループ構造が存在すると、プログラムが永遠に自分を呼び出し続けてスタックオーバーフローを起こします。
- 対策:本番のシステムでは、すでに通過したIDをコレクション(Collection)やDictionary等で記録しておき、「二度通ったIDはスキップする」というガード節を入れましょう。
- 罠2:パラメータの型ミスによる構文エラー
- 原因:SQLの文字列結合をする際、IDが数値型なのにシングルクォーテーションで囲んでしまったり、逆に文字列型なのに囲まないとエラーになります。
- 対策:数値型であれば今回のようにそのまま連結し、文字列であれば `'” & val & “‘` のように正しくクォーテーションで囲むことを忘れないようにしましょう。
—
まとめ
お疲れ様でした!今回は少し歯応えのある「QueryDefの再帰呼び出しによる階層データ抽出」を解説しました。
- QueryDefは実行中にSQLを書き換えて再利用できる
- 「見つかった結果を次の親にして自分を呼ぶ」のが再帰のアルゴリズム
- メモリの解放(クローズとNothing)は再帰の命綱
ここをクリアできたあなたは、単なる「マクロ記録の延長」から、ロジックを組み立てられる本格的なAccess VBAエンジニアの領域に足を踏み入れています。
ぜひご自身の開発環境でも試して、イミディエイトウィンドウに美しく展開されるツリー構造を味わってみてください。あなたのAccess開発ライフが、より一層楽しく、クリエイティブなものになりますように!
