Access VBAを掌握する極限の知見:DAO.Recordset「FindFirst」と「NoMatch」による超高速レコードジャンプ術
開発現場を見渡すと、「特定の条件に一致するデータを探す」という極めてプリミティブな要件に対し、毎回のように愚直なSQLを発行したり、フォームの`Filter`プロパティをこねくり回したりしているコードに遭遇する。
ハッキリ言おう。それはAccessのオブジェクトモデルの特性を無視した、非効率の極みだ。
特に、数万件規模のローカルテーブルやネットワーク上のBE(バックエンド)データベースを相手にするシステムにおいて、フォームのフィルタリングや毎回生成するクエリは、パフォーマンスのボトルネックになりやすい。
今回は、DAO(Data Access Objects)の真髄である `FindFirst` メソッドと `NoMatch` プロパティを組み合わせ、クエリを一切介さずにメモリ上で瞬時にターゲットレコードへジャンプし、フォームの同期を取る極限のテクニックを伝授する。
—
なぜ「クエリやフォームフィルタ」ではなく「DAO Recordset」なのか?
実務でよくあるアンチパターンを見てみよう。
- アンチパターンA: 検索条件が変わるたびに `Me.RecordSource = “SELECT FROM T_受注 WHERE 顧客ID = ” & id` とフォームのソース自体を書き換える。
- ⇒ 弊害: フォームの再描画コストが発生し、カレントレコードの位置がリセットされ、何よりコードが肥大化する。
- アンチパターンB: `DoCmd.ApplyFilter` を使う。
- ⇒ 弊害: ネットワーク経由(ファイル共有等)の場合、Jet/ACEエンジンとAccessクライアント間で無駄なトラフィックとロック競合を生む。
DAOを使う圧倒的な優位性
`CurrentDb.OpenRecordset` で取得するレコードセット(特にテーブルタイプやインデックスが効いたダイナセット)は、Accessのエンジン内部で最適化されたメモリ空間に展開される。
そのメモリ上のカーソルを直接操作するのが `FindFirst` だ。SQLのパースコストすら発生させず、ミリ秒単位のジャンプを実現できる。
—
堅牢な設計:FindFirst & NoMatch の基本イディオム
`FindFirst` を実装する上で、プログラマが絶対に押さえなければならない鉄則が2つある。
1. 検索対象は「ダイナセット型(dbOpenDynaset)」または「テーブル型(dbOpenTable)」であること。 (スナップショット型では更新やブックマークの同期に制限が出る)
2. `FindFirst` 実行直後に必ず `NoMatch` プロパティを評価すること。
もし合致するレコードが存在しない状態でブックマーク操作などを続行すれば、即座に実行時エラー(エラー3021:カレント レコードがありません。)が爆誕する。
基本コード構造
Dim db As DAO.Database
Dim rs As DAO.Recordset
Set db = CurrentDb
‘ フォームのレコードソースと同一のテーブル/クエリをダイナセットで開く
Set rs = db.OpenRecordset(“T_顧客マスター”, dbOpenDynaset)
‘ 検索条件の指定(SQLのWHERE句と同じ構文。ただし文字列や日付の囲みに注意)
Dim criteria As String
criteria = “顧客コード = ‘C00123′”
rs.FindFirst criteria
If rs.NoMatch Then
MsgBox “該当するデータはありません。”, vbExclamation, “検索結果”
Else
‘ 見つかった場合の処理(フォームのブックマークを同期させる等)
End If
‘ 健全なクローズ処理
rs.Close
Set rs = Nothing
Set db = Nothing
—
【プロダクションコード】フォームと完全同期する実戦的ジェネレーター
ここからが本題だ。
「検索窓に入力されたキーをもとに、現在開いている非連結、あるいは連結フォームの該当レコードへ一瞬でフォーカスを移動させる」という、実務で100回は実装する共通処理を最高品質で書く。
以下のコードは、エラーハンドリング、トランザクションの意識、オブジェクトの確実な解放(メモリリーク防止)を網羅した、そのまま現場に投入できる実戦コードである。
‘ ==============================================================================
‘ モジュール名: 業務共通標準モジュール (mdlNavigation)
‘ 処理概要 : DAO.FindFirstを用いた高速レコードジャンプ
‘ ==============================================================================
Public Sub JumpToRecord(ByRef targetForm As Form, _
ByVal sourceTableName As String, _
ByVal primaryKeyName As String, _
ByVal targetValue As Variant)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim criteria As String
On Error GoTo ErrorHandler
‘ 1. 引数のバリデーション(プロフェッショナルは入り口で弾く)
If targetForm Is Nothing Then Exit Sub
If Len(sourceTableName) = 0 Or Len(primaryKeyName) = 0 Then
Err.Raise 9999, , “パラメータが不正です。テーブル名とキーを指定してください。”
End If
Set db = CurrentDb
‘ 2. 高速なダイナセットでレコードセットを開く
Set rs = db.OpenRecordset(sourceTableName, dbOpenDynaset)
‘ 3. 条件式の構築(データ型によるエスケープの考慮)
Select Case VarType(targetValue)
Case vbString
criteria = primaryKeyName & ” = ‘” & Replace(targetValue, “‘”, “””) & “‘”
Case vbDate
criteria = primaryKeyName & ” = #” & Format(targetValue, “yyyy/mm/dd hh:nn:ss”) & “#”
Case Else
criteria = primaryKeyName & ” = ” & targetValue
End Select
‘ 4. FindFirstの実行
rs.FindFirst criteria
‘ 5. NoMatchの判定とジャンプ処理
If rs.NoMatch Then
MsgBox “指定されたレコードは存在しません。”, vbInformation, “検索”
Else
‘ フォームのBookmarkプロパティに、RecordsetのBookmarkをバインドする
‘ これにより、フォームの表示位置が瞬時にジャンプする
targetForm.Bookmark = rs.Bookmark
End If
CleanUp:
‘ 6. オブジェクトの確実な解放(メモリリークの根絶)
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error: ” & Err.Description, vbCritical, “システムエラー”
Resume CleanUp
End Sub
このコードのアーキテクチャ上の優位性
1. SQLインジェクション対策への配慮: `Replace(targetValue, “‘”, “””)` を挟むことで、文字列検索時にシングルクォーテーションが含まれていても構文エラーを防ぐ。
2. データ型の動的判定: `VarType` を用いて、数値・文字列・日付のそれぞれに応じた適切なSQLリテラル形式に自動変換している。
3. `Form.Bookmark` との同期: `rs.Bookmark = targetForm.Bookmark` ではなく、逆方向である `targetForm.Bookmark = rs.Bookmark` を行うことで、DAO側で見つけた物理的な位置情報をフォーム側へダイレクトに流し込んでいる。これが最も高速な同期手法である。
—
開発現場で絶対にハマる「3つの罠」と回避策
最後に、実務でこの手法を使う際に多くの開発者が踏み抜く地雷と、その回避策を共有する。
1. ネットワーク経由(Split構成)でのパフォーマンス劣化
BE(データ側)が別PCの共有フォルダーにある場合、テーブル全体をダイナセットで開く `OpenRecordset(“T_XXX”, dbOpenDynaset)` は、インデックスが適切に張られていないとネットワーク帯域を圧迫する。
- 対策: テーブル側に必ず主キー(Primary Key)または検索対象フィールドにインデックスを構築しておくこと。Jet/ACEエンジンはインデックスがあれば必要なページしかフェッチしないため、爆速を維持できる。
2. フォームのレコードソースと `OpenRecordset` の不一致
`JumpToRecord` プロシージャの引数に渡す `sourceTableName` が、フォームの `RecordSource`(複雑なJOINクエリ等)と異なっていた場合、`Bookmark` の互換性が失われ、実行時エラーになる。
- 対策: フォームが複雑なクエリをソースにしている場合は、そのクエリ名自体を `OpenRecordset` の第一引数に指定すること。
3. 排他制御とデッドロックの恐怖
`dbOpenDynaset` はデフォルトで共有モードだが、他のユーザーが該当レコードを編集中(編集ロック中)である場合、`FindFirst` 自体は成功しても、その後の編集やブックマーク移動で挙動がおかしくなることがある。
- 対策: 純粋な「参照・ジャンプ」目的であれば、第2引数に `dbReadOnly` を付与することを強く推奨する。
Set rs = db.OpenRecordset(sourceTableName, dbOpenDynaset, dbReadOnly)
これにより、不要なロック競合を回避し、パフォーマンスも向上する。
—
総括
DAOの `FindFirst` と `NoMatch` は、古臭いテクノロジーではない。
Accessというデータベースエンジンの構造を理解した上で使いこなせば、今なお現役の最強の高速検索インターフェースである。
非効率なクエリの乱発や、フォームの無駄な再描画からシステムを解放し、手元のメモリ空間を支配せよ。それこそが、プロフェッショナルなAccess開発者の仕事である。
