Form.RecordsetCloneを掌握せよ:UI描画コストを極限まで削ぎ落とす高速検索アーキテクチャ
レガシーシステムの寿命は、往々にして「画面のもたつき」というユーザーの小さなストレスの蓄積によって断たれる。
数万件のレコードを抱えるAccessのフォーム上で、コンボボックスの選択やテキストボックスの入力のたびに画面全体が再描画され、砂時計アイコンが点滅する――。この光景に絶望したシニアエンジニアは少なくないはずだ。
Access VBAにおけるパフォーマンスチューニングの金科玉条は「いかにJet/ACEデータベースエンジンとUIの結合度を下げ、描画コストを排除するか」に尽きる。
今回は、フォームの`RecordsetClone`プロパティを軸に、バックグラウンドで高速にデータ検索・同期を行う極限のテクニックを解説する。教科書通りの「`Find`メソッドの解説」で終わる気はない。オブジェクトのライフサイクルの実態、メモリリークの回避、そして数百万件規模をも視野に入れた実戦的アーキテクチャを紐解く。
—
1. なぜ通常の検索処理は遅いのか?(アーキテクチャの急所)
多くの開発者が陥るアンチパターンは、検索条件を変更するたびに `Me.Filter` と `Me.FilterOn = True` を叩くこと、あるいはフォームの `Recordset` に対して直接 `FindFirst` を実行することだ。
フォームの `Me.Recordset` を直接操作することは、UIのビジュアルコンポーネント(テキストボックス、リストボックスなど)とデータソースのポインタが完全に同期していることを意味する。レコードを1件移動するたびに、Accessは以下の重い処理を同期的に強制される。
1. カレントレコードの変更に伴うフィールド値のUIコントロールへの転送。
2. 条件付き書式や入力規則の再評価。
3. グリッドやフォーム全体の強制再描画(Paintイベントの発生)。
これが「UIのモタつき」の正体だ。
解法:RecordsetClone という「影武者」
`Form.RecordsetClone` は、現在フォームにバインドされているレコードセットのクローン(複製)を返す。このクローンは、元のレコードセットとデータソース(テーブルやクエリ)を共有しつつも、UIのポインタ(カレントレコード位置)とは完全に切り離された独立したメモリ空間に存在する。
つまり、バックグラウンドの「影武者」である `RecordsetClone` 側で高速に検索を行い、合致したレコードのブックマーク(Bookmark)だけを、瞬時にUI側の `Me.Bookmark` に引き渡せばいい。これだけで、描画コストの9割をバイパスできる。
—
2. 実装:極限まで最適化された検索・移動プロシージャ
実戦でそのまま使えるコードを示す。ここでは、入力ミスや存在しないキーを指定された際のエラーハンドリング、およびオブジェクトの厳格なライフサイクル管理を網羅している。
‘ ==============================================================================
‘ 担当: チーフアーキテクト
‘ 概要: RecordsetCloneを用いた高速レコード検索・同期プロシージャ
‘ 備考: 検索キーが見つからない場合のフォールバックと、メモリ管理を徹底
‘ ==============================================================================
Public Sub FastSearchAndNavigate(ByVal targetField As String, ByVal searchValue As Variant)
Dim rsClone As DAO.Recordset
‘ フォームにレコードがバインドされていない、またはデータがない場合は即座に抜ける
If Me.NewRecord And Me.Recordset.RecordCount = 0 Then Exit Sub
On Error GoTo ErrorHandler
‘ 1. RecordsetCloneの取得
‘ ※ 注意: Form.RecordsetCloneは呼び出すたびに新しいオブジェクトインスタンスを生成するため、
‘ 必ず変数に受けて明示的に解放する必要がある。
Set rsClone = Me.RecordsetClone
‘ 検索値がNullまたは長さ0の文字列の場合は処理をスキップ
If IsNull(searchValue) Or searchValue = “” Then
rsClone.Close
Set rsClone = Nothing
Exit Sub
End If
‘ 2. 検索条件の構築(データ型に応じたクエリリテラルのエスケープ)
Dim criteria As String
Select Case rsClone.Fields(targetField).Type
Case dbText, dbMemo, dbChar
‘ 文字列型の場合(シングルクォートのエスケープ処理を含む)
criteria = “[” & targetField & “] = ‘” & Replace(searchValue, “‘”, “””) & “‘”
‘ 完全一致にしたい場合は先頭の ” を削除し ‘=’ に変更すること
Case dbLong, dbInteger, dbByte, dbCurrency, dbSingle, dbDouble
‘ 数値型の場合
criteria = “[” & targetField & “] = ” & searchValue
Case dbDate
‘ 日付型の場合(USロケール形式に厳密に準拠させるか、#で囲む)
criteria = “[” & targetField & “] = #” & Format(searchValue, “yyyy-mm-dd hh:nn:ss”) & “#”
Case Else
‘ その他(Boolean等)
criteria = “[” & targetField & “] = ” & searchValue
End Select
‘ 3. バックグラウンドでの高速検索 (FindFirst)
‘ ※ Jet/ACEエンジン内部のインデックスが効いているため、数万件でも一瞬で終わる
rsClone.FindFirst criteria
‘ 4. 結果の判定とUIへのブックマーク転送
If rsClone.NoMatch Then
MsgBox “指定されたデータは存在しません。”, vbInformation, “検索結果”
Else
‘ ★ここが核心:UI側のブックマークをクローンのものに同期させる。
‘ これにより、UIの再描画は「最終的な1件の移動時」にのみ発生する。
Me.Bookmark = rsClone.Bookmark
End If
CleanUp:
‘ 5. オブジェクトの明示的解放(メモリリークの完全阻止)
If Not rsClone Is Nothing Then
rsClone.Close
Set rsClone = Nothing
End If
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical, “System Error”
Resume CleanUp
End Sub
—
3. チーフアーキテクトが教える、見落としがちな罠と極限の知見
この手法を導入するにあたり、現場で遭遇しがちな「罠」と、それをねじ伏せるための知見を共有しよう。
知見①:RecordsetCloneの「インスタンス生成コスト」と解放漏れ
`Me.RecordsetClone` プロパティは、プロパティであるにもかかわらず、呼び出すたびに新しいDAO.Recordsetオブジェクトのインスタンスをヒープ上に生成する。
これを `Me.RecordsetClone.FindFirst …` のように変数に受けずに対話的に呼び出すと、参照を失ったオブジェクトがガベージコレクションの処理待ちとなり、Accessのプロセス(MSACCESS.EXE)のメモリフットプリントが肥大化する(いわゆるメモリリークの温床となる)。
必ず変数に格納し、処理の最後には `.Close` と `Set … = Nothing` を実行すること。 これがプロとアマの決定的な境界線だ。
知見②:インデックスの恩恵を最大限に受けるためのクエリ設計
`RecordsetClone.FindFirst` は、バックグラウンドでJet/ACEエンジンを直叩きするため、検索対象フィールドにインデックス(Index)が貼られていることが絶対条件だ。
もしフォームの元となるレコードソースが、複雑な集計クエリ(`GROUP BY` や `DISTINCT`、非連結の演算フィールドを含むもの)であった場合、DAOのクローンであってもインデックスが効かかず、テーブルスキャン(全件走査)が発生して激しくパフォーマンスが劣化する。
高速検索を目的とするフォームのレコードソースは、可能な限り「単一テーブル」または「インデックスが完全に効く最適化されたビュー/クエリ」に絞るべきである。
知見③:非同期UIイベント(TextBoxのChangeイベント)との組み合わせ
ユーザーのタイピングに合わせてリアルタイム検索(インクリメンタルサーチ)を実装したい場合、テキストボックスの `Change` イベントで上記のプロシージャを呼び出すことになる。
しかし、すべてのキーストローク(1文字入力するごと)でクローン検索走査走ると、かえって入力の邪魔になる場合がある。
シニアなアプローチとしては、以下のようにWindows API(`Timer` やカスタムループ)を用いた簡易的なデバウンス(入力遅延制御)を挟むか、タイマーイベント(`Form_Timer`)を組み合わせることで、無駄なクエリ発行を極限まで抑制する設計が求められる。
‘ リアルタイム検索を行う場合の簡易的なタイマー制御の概念
Private Sub txtSearchBox_Change()
‘ タイマーインターバルを一旦リセット(デバウンス処理)
Me.TimerInterval = 300 ‘ 0.3秒間入力を待つ
End Sub
Private Sub Form_Timer()
‘ タイマー発火時に検索を実行し、タイマーを停止する
Me.TimerInterval = 0
Call FastSearchAndNavigate(“ProductName”, Me.txtSearchBox.Text)
‘ フォーカスが外れないようにキャレット位置を復元
Me.txtSearchBox.SetFocus
Me.txtSearchBox.SelStart = Len(Me.txtSearchBox.Text)
End Sub
—
最後に:レガシーの殻を破るエンジニアリングを
「Accessだから遅いのは仕方ない」――そんな言い訳は、オブジェクトのライフサイクルとデータベースエンジンの挙動を正しく理解していない者の逃げに過ぎない。
`Form.RecordsetClone` は、単なるレガシーな機能ではない。UIとデータ層をエレガントに切り離し、リソースを最小限に抑えながら最高のユーザー体験(UX)をねじ込むための、先人たちの知的財産である。
この知見をあなたのシステムに実装し、軽快に疾走するAccessアプリケーションを目の当たりにしてほしい。技術の本質を知る者だけが、レガシーを最先端の武器に変えることができる。
