【テクニカル・上級編】フォームのRecordsetCloneとBookmarkを同期させ、非連結フォームで検索結果を表示するテクニック – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:RecordsetCloneとBookmarkの同期による高速検索UIの構築

レガシーシステムの寿命は、多くの場合、そのUIの応答性とメンテナンス性に依存している。特にMicrosoft Accessにおいて、「連結フォームの利便性(データバインドの自動化)」と「非連結フォームのような自由な検索・絞り込み」を両立させることは、長年多くのアーキテクトを悩ませてきたテーマだ。

全レコードを毎回 `Requery` するような愚行は、ネットワーク帯域とJet/ACEエンジンのトランザクションログを無駄に消費するだけである。真にスケーラブルなAccessアプリケーションを構築するためには、`RecordsetClone` と `Bookmark` のメカニズムを完全に理解し、メモリ空間とUIスレッドを調停しなければならない。

今回は、数百万件規模のデータセットを抱える現場において、フォームのパフォーマンスを極限まで引き出すための同期テクニックを解説する。

1. アーキテクチャの核心:なぜ RecordsetClone なのか

フォームにバインドされたレコードソース(RecordSource)は、常にカレントレコードポインタとGUIの描画状態が密結合している。ここで安易に `Me.RecordSource = “SELECT FROM … WHERE …”` を実行すると、以下の深刻なペナルティが発生する。

1. ビューポートの再構築コスト: フォーム全体のコントロールが再描画され、ちらつき(フリッカー)が発生する。
2. カレントレコードの喪失: 検索条件変更のたびに先頭レコードへ強制送還される。
3. Jet/ACEエンジンのキャッシュ効率低下: クエリの再コンパイルとロックのオーバーヘッド。

ここで登場するのが `RecordsetClone` プロパティだ。
`RecordsetClone` は、フォームが保持しているレコードセットのクローン(複製された独立したレコードセットポインタ)をメモリ上に展開する。これに対し、フォーム本体のレコードセットとは独立して `Find` メソッドや移動を行えるため、「裏側で高速に検索を行い、ヒットした位置(Bookmark)だけをフォーム本体に教える」という分離アーキテクチャが成立する。

2. 内部挙動とメモリ管理の鉄則

シニアエンジニアが絶対に押さえておかなければならないのは、「VBAにおけるオブジェクトのライフサイクルと解放の重要性」である。

`Me.RecordsetClone` を呼び出すたびに、Accessは内部的にDAOのレコードセットオブジェクトの参照を返す。これを適切に変数に格納して操作する場合、明示的なクローズとNothing代入を行わないと、フォームのアンロード後もメモリリークを引き起こす

また、`Bookmark` はバイナリデータ(バイト配列)であり、Jet/ACEデータベースエンジン固有の行識別子である。SQL Server(ODBC/ADO透過的接続)をバックエンドにしている場合、Bookmarkの挙動やパフォーマンス特性が変化するため、ODBCDirectやADODBとの違いを意識した設計が不可欠となる。

3. 実装:検索結果の同期と Bookmark 制御の実践コード

以下のコードは、検索用テキストボックス(`txtKeyword`)に入力された文字列をもとに、メインフォーム(連結フォーム)の表示位置を高精度かつノータイムで同期させる実用モジュールである。

エラーハンドリングとオブジェクトの解放を完全に網羅した、プロダクション品質のコードを提示する。

Option Compare Database
Option Explicit

‘ ==============================================================================
‘ フォーム名: frmOrderManagement
‘ テーマ: RecordsetClone と Bookmark を使用した高速検索・同期処理
‘ ==============================================================================

Private Sub cmdSearch_Click()
On Error GoTo ErrorHandler

Dim db As DAO.Database
Dim rsClone As DAO.Recordset
Dim strKeyword As String
Dim strCriteria As String

‘ 1. 入力値のサニタイジングと検証
strKeyword = Trim(Me.txtKeyword.Value & “”)
If Len(strKeyword) = 0 Then
MsgBox “検索キーワードを入力してください。”, vbExclamation, “入力エラー”
Me.txtKeyword.SetFocus
Exit Sub
End If

‘ SQLインジェクション等を防ぐためのエスケープ処理(シングルクォートの二重化)
strKeyword = Replace(strKeyword, “‘”, “””)

‘ 2. フォームの RecordsetClone を取得
‘ ※ 注意: Newキーワードは使用せず、フォームが持つプロパティを参照する
Set rsClone = Me.RecordsetClone

‘ 3. 検索条件の構築(例: 顧客名または注文番号の部分一致)
‘ ※ インデックスが効かない前方一致・中間一致の場合でも、Clone操作はフォーム描画より圧倒的に高速
strCriteria = “CustomerName LIKE ‘” & strKeyword & “‘ OR OrderNumber LIKE ‘” & strKeyword & “‘”

‘ 4. クローンレコードセット側で条件に一致するレコードを検索
rsClone.FindFirst strCriteria

‘ 5. 検索結果の判定とBookmarkの同期
If rsClone.NoMatch Then
MsgBox “一致するレコードが見つかりませんでした。”, vbInformation, “検索結果”
‘ 必要に応じてここでクローンを解放して抜ける
Else
‘ 【核心】クローンで見つけた位置(Bookmark)を、メインフォームに転送する
‘ これにより、フォームの再描画コストを最小限に抑えつつ、該当レコードへジャンプできる
Me.Bookmark = rsClone.Bookmark

‘ 視覚的なフィードバックとしてフォーカスを目当てのコントロールへ移す
If Me.AllowEdits Then
Me.CustomerName.SetFocus
End If
End If

CleanUp:
‘ 6. オブジェクトの明示的解放(メモリリークの完全防止)
‘ ※ RecordsetClone 自体はフォームが管理しているが、取得した参照変数は確実に解放する
If Not rsClone Is Nothing Then
rsClone.Close
Set rsClone = Nothing
पाओ Exit Sub

ErrorHandler:
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical, “システムエラー”
Resume CleanUp
End Sub

Private Sub cmdClear_Click()
On Error GoTo ErrorHandler

‘ 検索条件のクリアと先頭レコードへの移動
Me.txtKeyword.Value = “”

Dim rsClone As DAO.Recordset
Set rsClone = Me.RecordsetClone

If Not rsClone.EOF Then
rsClone.MoveFirst
Me.Bookmark = rsClone.Bookmark
End If

CleanUp:
If Not rsClone Is Nothing Then
rsClone.Close
Set rsClone = Nothing
End If
Exit Sub

ErrorHandler:
Resume CleanUp
End Sub

4. チーフアーキテクトからの実践的アドバイス:エッジケースと最適化

このテクニックを実際の巨大な業務システムに組み込む際、以下の「罠」に直面することがある。プロフェッショナルとしてこれらを事前に潰しておかねばならない。

① フォームが「フィルター(Filter)」されている場合の挙動

フォームに `Me.Filter` が適用されている場合、`Me.RecordsetClone` はフィルター済みのレコードセットを返す。
もし「全レコードから検索したい」という要件がある場合は、フォームの `RecordSource` を直接 DAO で開き直すか、あるいは `CurrentDb.OpenRecordset` を用いた別ルートの検索ロジックを検討する必要がある。UIとの同期を最優先するなら、フィルターは解除した状態で `RecordsetClone` を叩くのがセオリーだ。

② 複数条件の動的クエリとインデックス設計

`FindFirst` は便利だが、内部的には逐次検索(テーブルスキャン)に近い挙動をとる場合がある。
バックエンドが Access (accdb) であり、数万件以上のオーダーがある場合は、検索対象フィールドに対してあらかじめインデックス(Index)を貼っておくこと。これだけで `FindFirst` の速度が数倍〜数十倍に跳ね上がる。

③ バックエンドが SQL Server (ADP / ODBC) の場合の注意点

SQL Server をリンクテーブルとして接続している場合、`Bookmark` のデータ型やサイズが変更されることがある。特に古いODBCドライバを使用している環境では、Bookmarkの同期時にエラー(実行時エラー 3159: 無効なブックマークなど)が発生するリスクがある。
このようなレガシー/ハイブリッド環境では、Bookmarkの直接代入ではなく、主キー(Primary Key)の値を保持させて `Me.Recordset.FindFirst “ID = ” & targetID` で位置を特定するフォールバック処理を実装しておくと、システムの堅牢性が劇的に向上する。

総括

Access VBAは、しばしば「おもちゃの言語」と揶揄されることがある。しかし、それは言語の限界ではなく、使う側のアーキテクチャ理解の欠如に起因することがほとんどだ。

`RecordsetClone` と `Bookmark` を組み合わせたこの非同期・同期ハイブリッドUIパターンをマスターすれば、Accessとは思えないほど軽快で、モダンなWebアプリケーションに匹敵するレスポンスを持つ業務システムを構築できる。
メモリのライフサイクルを慈しみ、オブジェクトの挙動を完全に掌握した者だけが、真のレガシーキラーとなれるのだ。

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