【Access VBA極限の知見】RecordsetCloneとBookmarkの神髄:非連結検索フォームを実務レベルで完全掌握する
こんにちは。チーフアーキテクトの私だ。
日々、現場からの「Accessが重い」「検索フォームの挙動がおかしい」「マルチユーザー環境でエラー吐いて止まる」という悲鳴を聞くのに飽き飽きしていないか?
Access開発において、「検索条件を指定してヒットしたレコードへスムーズにジャンプさせたい」という要求は、もはや避けて通れない基本要件だ。しかし、この実装で多くの初学者、いや、中途半端なベテランすらもが犯す致命的なミスがある。
それが、「検索のたびに `Me.RecordSource` を書き換える」 という愚行だ。
画面がチラつき、マルチユーザー環境では排他制御のロック競合を引き起こし、何よりコードの保守性をドブに捨てるようなそのアプローチは、今日この瞬間から封印してほしい。
今回は、Accessオブジェクトモデルの深淵を突く「RecordsetClone」と「Bookmark」の同期テクニックを授けよう。連結フォームの利便性を1ミリも損なわず、爆速かつ堅牢な検索・表示UIを構築するプロの技法を解説する。
—
なぜ `RecordSource` の動的書き換えは「悪」なのか?
検索ボタンを押されるたびに、以下のようなコードを書いている者はいないか?
‘ 【絶対に真似してはならないアンチパターン】
Private Sub btnSearch_v1_Click()
‘ 検索のたびにSQLを再発行し、フォームの基盤をごっそり入れ替える
Me.RecordSource = “SELECT FROM T_Customer WHERE CustomerName LIKE ‘” & Me.txtKeyword.Text & “‘”
Me.Requery
End Sub
一見動くように見えるこのコードは、実務の現場では地雷原だ。
なぜなら:
1. パフォーマンスの殺戮: フォームが保持するすべてのレコードキャッシュが破棄され、Jet/ACEエンジンに無駄なクエリコンパイルとI/Oを強いる。
2. カーソル位置の喪失: レコードソースを書き換えると、カレントレコードは強制的に先頭へジャンプし、ユーザーがどこを見ていたのかが分からなくなる。
3. トランザクションとロックの肥大化: フォーム自体のデータソースを頻繁に変更することは、マルチユーザー環境において意図しないレコードロックの範囲拡大を招く。
究極のアーキテクチャ:非連結コントロール+RecordsetClone
プロが目指すべき設計思想はこうだ。
- フォームの `RecordSource` は固定し、フォーム起動時に一度だけバインドする。
- 検索は「テキストボックス(非連結)」等で受け付ける。
- フォームが裏で持っているクエリの結果セット(`RecordsetClone`)に対してヒット位置を割り出し、`Bookmark`(ブックマーク)を使って一瞬でカーソルをジャンプさせる。
このアプローチにより、データベースへの無駄な負荷を消し去り、UIの反応速度を極限まで高めることが可能になる。
—
実装の全体像とオブジェクトのライフサイクル
Accessのフォームオブジェクトには、裏でメモリ上に展開されているデータへの参照である `RecordsetClone` という強力なプロパティが存在する。
フォーム本体のレコードポインタ(カレントレコード)と、`RecordsetClone` のポインタは完全に独立している。つまり、ユーザーに画面のちらつきを見せることなく、裏側のクローンレコードセット側で自由自在に検索(`FindFirst` など)を行い、合致したレコードの「住所」である `Bookmark` を取得して、それをフォーム本隊に教えてやればいいのだ。
—
【実践】コピペで動くプロダクションコード
以下のコードは、顧客管理テーブル(`T_Customer`)を想定した、実務でそのまま使える堅牢な検索・同期ロジックだ。エラーハンドリング(防衛的プログラミング)も完全に網羅してある。
前提条件
- フォームのレコードソース(`RecordSource`)には、あらかじめ `T_Customer`(または十分なパフォーマンスを持つクエリ)が設定されていること。
- 検索キーワードを入力する非連結のテキストボックス名が `txtSearchKeyword` であること。
- ジャンプ対象の主キー(ID)フィールド名が `CustomerID`(数値型)であること。
Option Compare Database
Option Explicit
‘ ==============================================================================
‘ 担当者コード: 検索キーワードに基づき、RecordsetCloneとBookmarkを用いて
‘ フォームの表示位置を該当レコードへ高速同期させるプロシージャ
‘ ==============================================================================
Private Sub btnExecuteSearch_Click()
On Error GoTo ErrorHandler
Dim rsClone As DAO.Recordset
Dim strKeyword As String
Dim strCriteria As String
‘ 1. 入力値の検証とサニタイジング(ワイルドカードのエスケープ等)
If Trim(Me.txtSearchKeyword.Value & “”) = “” Then
MsgBox “検索キーワードを入力してください。”, vbExclamation, “入力チェック”
Me.txtSearchKeyword.SetFocus
Exit Sub
End If
‘ SQLインジェクションや特殊文字対策としてシングルクォーテーションをエスケープ
strKeyword = Replace(Me.txtSearchKeyword.Value, “‘”, “””)
‘ 2. フォームのRecordsetCloneを取得
‘ ※注意: Closeメソッドを呼ぶ必要はない。フォームのライフサイクルに依存する。
Set rsClone = Me.RecordsetClone
‘ 3. レコードが存在するかどうかのチェック
If rsClone.RecordCount = 0 Then
MsgBox “検索対象となるデータが存在しません。”, vbInformation, “通知”
Set rsClone = Nothing
Exit Sub
End If
‘ 4. 検索条件の構築 (例: 顧客名または担当者名部分一致)
‘ ※必要に応じてフィールド名を実際の設計に合わせて変更すること
strCriteria = “CustomerName LIKE ‘” & strKeyword & “‘ OR ContactPerson LIKE ‘” & strKeyword & “‘”
‘ 5. クローンレコードセットに対してFindFirstを実行
rsClone.FindFirst strCriteria
‘ 6. 検索結果の判定と同期処理
If rsClone.NoMatch Then
‘ 該当なしの場合
MsgBox “一致するレコードが見つかりませんでした。”, vbInformation, “検索結果”
Else
‘ 7. 【核心】見つかったレコードのBookmarkをフォーム本体に渡す
Me.Bookmark = rsClone.Bookmark
‘ 視覚的なフィードバック(必要に応じてフォーカス移動)
‘ Me.CustomerName.SetFocus
End If
CleanUp:
‘ 8. オブジェクト変数の解放(メモリリークの防止)
Set rsClone = Nothing
Exit Sub
ErrorHandler:
‘ 予期せぬエラーの捕捉
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical, “システムエラー”
Resume CleanUp
End Sub
—
現場で絶対に知っておくべき「技術的罠」と回避策
このコードを実装するにあたり、チーフアーキテクトとしていくつか現場の罠を警告しておこう。
1. `RecordsetClone` の型は必ず `DAO.Recordset` にせよ
ADODBではなく、DAO であることに注意せよ。Access VBAにおいてフォームが内部保持するレコードセットは常にDAOのレイヤーで動いている。`Dim rs As Recordset` と曖昧に宣言すると、参照設定の優先順位によってADOと誤認され、`Bookmark` 代入時に「型が一致しません」という不可解なエラーで夜を明かすことになる。必ず `DAO.Recordset` と明示せよ。
2. マルチユーザー環境における Bookmark の無効化
Aさんがレコードを削除した直後、Bさんが古い `Bookmark` を保持したままジャンプしようとすると、Accessは容赦なく「実行時エラー ‘3158’: ブックマークが無効です」をスローする。
これを防ぐためには、`FindFirst` の前に `rsClone.Requery` を挟むか、あるいはエラーハンドラ内で `Err.Number = 3158` をキャッチし、「他のユーザーによってデータが変更または削除されました。検索をやり直してください」と優しくユーザーに導くフェイルセーフな設計が不可欠だ。
3. パフォーマンスの境界線
`RecordsetClone` + `FindFirst` は、ローカルDB(数万〜数十万レコード)であれば秒速で動作する。しかし、バックエンドがSQL ServerなどのODBCリンクテーブルであり、数百万レコードを超える規模である場合、`FindFirst` はクライアント側での逐次スキャン(あるいは非効率なクエリ発行)を引き起こすことがある。
その場合は、インデックスが適切に張られているかを確認し、必要であれば `ServerFilter` や適切なパススルー・クエリの併用を検討すべきだ。だが、一般的な業務アプリの検索UIであれば、今回紹介した手法で十分すぎるほどのパフォーマンスが出せるはずだ。
—
最後に:コードの品格を保て
Accessは「誰でも簡単に作れる」がゆえに、スパゲッティコードの温床になりやすい。
しかし、今回解説した `RecordsetClone` と `Bookmark` の同期メカニズムを正しく理解し実装できれば、あなたの作るAccessアプリは、市販のパッケージソフトに匹敵する軽快で洗練されたUXを手に入れる。
「動けばいいや」の精神を捨て、細部までこだわり抜いたアーキテクチャを構築してほしい。君の健闘を祈る。
