Access VBAを掌握する極限の知見:RecordsetCloneとBookmarkの神髄
こんにちは。開発プロジェクトの現場で、数々のレガシーなAccessシステムをモダンで堅牢なアーキテクチャへと蘇らせてきたチーフアーキテクトだ。
Access開発において、「フォームの検索・絞り込み処理」は避けて通れない要件である。しかし、未熟なコードでは、検索ボタンを押すたびにテーブルやクエリへ無駄なクエリを発行し、フォームの再クエリ(`Requery`)によって画面がちらつき、ユーザーの入力カーソル位置が吹き飛ぶという地獄のようなUIを生み出している。
君たちは、ユーザーが検索を行うたびに、フォーム全体のデータソースを書き換えていないか?
もし `Me.RecordSource = “SELECT …”` などと動的に書き換えているなら、直ちにその手を止めてほしい。それはパフォーマンスの観点からも、メモリ管理の観点からも最悪のアンチパターンだ。
今回は、フォームのデータソースを汚さず、`RecordsetClone` と `Bookmark` を完璧に同期させることで、爆速かつ画面のちらつきゼロの検索結果同期テクニックを授けよう。
—
1. なぜ「RecordSourceの動的変更」は悪なのか?
初級プログラマーがやりがちな間違いはこうだ:
‘ 【アンチパターン】絶対にやってはいけない例
Private Sub btnSearch_v1_Click()
‘ 検索条件に合わせてRecordSourceを丸ごと書き換える
Me.RecordSource = “SELECT FROM T_Employees WHERE Name LIKE ‘” & Me.txtKeyword.Value & “‘”
Me.Requery
End If
このアプローチの問題点は明白だ。
1. ネットワーク/ローカルI/Oの無駄遣い: フォームが保持するメインのレコードセット(物理的なデータバインド)が破棄され、再構築されるため、重い。
2. カレントレコードの喪失: `Requery` が走ると、フォームの表示位置(カレントレコード)が強制的に先頭に戻る。ユーザーは「さっき見ていた行」を見失う。
3. カスケードするイベントの嵐: `Current` イベントや `Dirty` イベントが意図せず再発火し、予期せぬバグの温床となる。
究極の解:フォームとクローンを分離せよ
Accessフォームには、現在開いているレコードセットの「複製(クローン)」をメモリ上に保持する `RecordsetClone` という強力なプロパティが存在する。
このクローンは、元のフォームのレコードセットとは独立したカレントレコードポインタを持つ。つまり、「裏でクローンを検索し、ヒットした行のブックマーク(Bookmark)を、フォーム本体のブックマークにスナップさせる」という設計をとれば、フォームのデータソース一切を書き換えることなく、一瞬で表示を同期できるのだ。
—
2. アーキテクチャ設計:同期メカニズムの全体像
今回の実装におけるデータフローは以下の通りである。
1. ユーザーが検索キーワードを入力し、検索を実行。
2. フォームの `RecordsetClone` に対して `FindFirst` メソッドを実行し、合致するレコードを検索。
3. ヒットした場合、クローンの `Bookmark` を取得。
4. フォーム本体の `Bookmark` に、その値を代入。
5. 結果: フォームの表示位置が、一瞬で該当レコードへジャンプする。
※もし「複数件の絞り込み(フィルター)」を行いたい場合は、`RecordsetClone.Filter` と `RecordsetClone.OpenRecordset` を組み合わせるが、今回は「単一の検索結果への高速ジャンプおよび非連結フォーム風の制御」にフォーカスした最も実用性の高いコードを提示する。
—
3. プロダクションコード実装
以下のコードは、エラーハンドリング、オブジェクトのライフサイクル管理(メモリリーク防止)、そしてエッジケース(該当データなし)への配慮を完璧に網羅した、実務でそのまま使えるプロダクションコードだ。
‘ ==============================================================================
‘ モジュール名: フォームモジュール (例: F_EmployeeSearch)
‘ 概要: RecordsetCloneとBookmarkを活用した高速検索・同期処理
‘ ==============================================================================
Option Compare Database
Option Explicit
Private Sub btnSearch_Click()
On Error GoTo ErrorHandler
Dim rsCloned As DAO.Recordset
Dim criteria As String
Dim keyword As String
‘ 1. 入力値のサニタイジングとバリデーション
If Trim(Nz(Me.txtKeyword.Value, “”)) = “” Then
MsgBox “検索キーワードを入力してください。”, vbExclamation, “入力エラー”
Me.txtKeyword.SetFocus
Exit Sub
End If
keyword = Me.txtKeyword.Value
‘ 2. フォームのRecordsetCloneを取得
‘ ※ 注意: RecordsetCloneを取得する際、フォームにレコードが存在しない場合はNothingを返すことがある
Set rsCloned = Me.RecordsetClone
If rsCloned.RecordCount = 0 Then
MsgBox “対象となるデータが一切存在しません。”, vbInformation, “通知”
Exit Sub
End If
‘ 3. 検索条件の構築 (SQLインジェクション対策としてシングルクォーテーションのエスケープを考慮)
‘ ここでは社員名を部分一致で検索する例とする
criteria = “[EmployeeName] LIKE ‘” & Replace(keyword, “‘”, “””) & “‘”
‘ 4. クローン側でレコードを検索
rsCloned.FindFirst criteria
‘ 5. 検索結果の判定と同期
If rsCloned.NoMatch Then
MsgBox “一致するレコードが見つかりませんでした。”, vbInformation, “検索結果”
‘ 必要に応じて検索窓にフォーカスを戻すなどの処理
Else
‘ ★極意: クローンのBookmarkをフォームのBookmarkに代入することで、
‘ フォーム側のカレントレコードが一瞬で同期される(再クエリ不要)
Me.Bookmark = rsCloned.Bookmark
‘ 視覚的なフィードバック
Me.txtKeyword.SelStart = 0
Me.txtKeyword.SelLength = Len(Me.txtKeyword.Value)
End If
CleanUp:
‘ 6. オブジェクトの解放 (RecordsetCloneの参照は明示的にSet Nothingする)
‘ ※注意: RecordsetClone自体をCloseしてはならない。フォームの動作に悪影響を与えます。
Set rsCloned = Nothing
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical, “システムエラー”
Resume CleanUp
End Sub
—
4. チーフアーキテクトが教える「実装上の罠と極意」
このコードを現場に導入する際、シニアエンジニアとして知っておくべき「落とし穴」がいくつかある。
① `RecordsetClone` は `.Close` するな
初心者がやりがちな最大のミスが、`rsCloned.Close` を叩いてしまうことだ。
`RecordsetClone` は、Accessフォームが内部で管理しているレコードセットの「参照」を返しているに過ぎない。これを閉じるとフォーム自体のデータ基盤が崩壊し、最悪の場合Accessが強制終了(クラッシュ)する。
ルール: `Set rsCloned = Nothing` によるポインタの解放のみを行い、`.Close` は絶対に書くな。
② ブックマークの互換性(マルチユーザー環境とトランザクション)
`Bookmark` は、DAOレコードセットにおける「その瞬間の物理的な位置を示すバイナリデータ」である。
もしバックエンドがSQL Server(ODBC接続)や、マルチユーザーが同時アクセスしている環境の場合、他のユーザーによって該当レコードが削除されていると、`Me.Bookmark = rsCloned.Bookmark` の実行時に「実行時エラー 3156: 変更または削除されたレコードが見つかりません」が発生する。
堅牢性を高めるためには、トランザクション制御や、エラーハンドリング内で `Resume Next` などのフォールバックを慎重に設計する必要がある。単一ローカルAccDBであれば今回紹介したコードで完璧に動作する。
③ 次件検索(「次を検索」ボタン)の実装への応用
この `RecordsetClone` の真価は、一度ヒットした後の「次を検索(FindNext)」に発揮される。
フォーム上に「次を検索」ボタンを置き、以下のように記述するだけで、高速なインクリメンタルサーチ風の挙動を実装できる。
Private Sub btnFindNext_Click()
Dim rsCloned As DAO.Recordset
Set rsCloned = Me.RecordsetClone
‘ 現在のフォームの位置を基準に同期させるため、一度クローンのBookmarkをフォームに合わせる
rsCloned.Bookmark = Me.Bookmark
‘ 次を検索
rsCloned.FindNext “[EmployeeName] LIKE ‘” & Me.txtKeyword.Value & “‘”
If rsCloned.NoMatch Then
MsgBox “これ以上の一致するレコードはありません。”, vbInformation, “検索”
rsCloned.MoveLast ‘ あるいは先頭に戻す制御
Else
Me.Bookmark = rsCloned.Bookmark
End If
Set rsCloned = Nothing
End Sub
—
5. まとめ
Access VBAは、設計思想を誤ると「重い、バグが多い、メンテできない」の三重苦を生み出す。しかし、オブジェクトモデルのライフサイクル(`RecordsetClone`, `Bookmark`)の本質を理解していれば、モダンなWebアプリケション並みに軽快で、実用性の高い業務システムを構築することが可能だ。
「フォームのデータソースを無暗にいじらない」。
この鉄則をチーム全体に徹底させ、ワンランク上のAccess開発者を目指してほしい。
健闘を祈る。
