DAO.Recordsetの「Bookmark」を極限まで使い倒す:グリッド操作の完全調律
Access VBAにおけるフォームのデータ操作において、最も頻繁に発生するストレスの源泉は何か。それは、「データの一括処理や並び替えを行った瞬間、画面が先頭行にジャンプし、ユーザーが苦労して探したレコードのコンテキストが木っ端みじんに吹き飛ぶ」という現象だ。
フォームの再クエリ(`Requery`)やレコードソースの再割り当てを行うたびに、フォーカスは強制的にBOF(Beginning of File)へ飛ばされる。これを防ぐために`Find`メソッドや`Seek`メソッドで力技の検索コードを書くプログラマがいるが、それは悪手である。重複データが存在する環境では誤作動を起こし、何よりコードの実行速度が致命的に遅くなる。
この問題を一撃で、かつ最もエレガントに解決するのが `DAO.Recordset` の `Bookmark` プロパティ である。
今回は、シニアエンジニアおよび大規模Accessシステムを保守するアーキテクトに向けて、`Bookmark` の本質と、メモリリークを排除した極限のグリッド制御手法を解説する。
—
1. Bookmarkの正体と、Accessオブジェクトモデルの裏側
`Bookmark` とは、レコードセット内の特定のレコードを指し示すバイナリデータ(`String`型として扱われるが、中身はポインタや内部IDの塊)である。
ここで重要なのは、Accessのフォーム(`Form.Recordset`)と、VBAで明示的に開くDAOのレコードセット(`CurrentDb.OpenRecordset`)では、メモリ上の実体が異なるという点だ。
中途半端な知識のコードを書くと、以下のような致命的なミスを犯す。
- フォームの `Recordset` オブジェクトを直接操作してカレント位置を見失う。
- トランザクション処理中にブックマークを保持し、ロック競合を起こす。
- オブジェクト変数を `Nothing` 解放せず、Accessのワークスペースを肥大化させる。
真に堅牢なグリッド操作を実現するためには、「操作対象のレコードセットのブックマークを退避させ、処理後に確実に戻す」 というライフサイクルを厳密にコード化しなければならない。
—
2. 実装パターン:処理前後のコンテキスト完全保持プロシージャ
以下のコードは、サブフォーム上のグリッドで何らかのバッチ処理(例:選択された複数レコード、あるいは特定の条件エンティティの一括更新)を行った後、ユーザーが直前まで見ていた行、およびフォーカスをミリ秒単位の狂いもなく復元するための実用コードである。
‘ ==============================================================================
‘ 概要: サブフォームのグリッド操作において、カレントレコードの位置を完璧に保持するプロシージャ
‘ ターゲット: Microsoft Access 2010 / 2013 / 2016 / 2019 / 365 (DAO 3.6 / ACCDB)
‘ ==============================================================================
Public Sub ExecuteGridOperationSafely(ByVal frmTarget As Form)
Dim rst As DAO.Recordset
Dim varBookmark As Variant
Dim lngTopRow As Long
‘ 画面描画の凍結(フリッカー防止とパフォーマンス劇的向上)
Application.Echo False
frmTarget.Painting = False
On Error GoTo ErrorHandler
‘ フォームのレコードセットクローンを取得(カレント位置を独立して保持するため必須)
Set rst = frmTarget.RecordsetClone
‘ レコードが存在しない場合は即座に抜ける
If rst.RecordCount = 0 Then GoTo Cleanup
‘ 1. 現在のブックマーク(行位置)を退避
If Not (rst.BOF And rst.EOF) Then
If frmTarget.NewRecord = False Then
varBookmark = frmTarget.Bookmark
End If
End If
‘ 2. グリッドのスクロール位置(上部に表示されている行)の退避
‘ ※Accessのバージョンやサブフォームの仕様によりスピン動作が異なるため、
‘ 古い環境ではコントロールプロパティの補正が必要になる場合がある
lngTopRow = frmTarget.SelTop
‘ ==============================================================================
‘ 【ここに実際のデータ処理・一括更新ロジックを記述】
‘ 例として、何らかのステータスフラグを反転させる処理を想定
‘ ==============================================================================
‘ CurrentDb.Execute “UPDATE T_Sample SET Status = 1 WHERE …”, dbFailOnError
‘ ==============================================================================
‘ 3. フォームのデータを再読み込み(ここでカレント位置が吹き飛ぶ)
frmTarget.Requery
‘ 4. ブックマークを使用して元のレコード位置へ復元
If Not IsEmpty(varBookmark) Then
‘ クローン側ではなく、フォーム自体のRecordsetに対してBookmarkを適用する
frmTarget.Bookmark = varBookmark
End If
Cleanup:
‘ オブジェクトの明示的解放(メモリリークの完全阻止)
If Not rst Is Nothing Then
rst.Close
Set rst = Nothing
End If
‘ 画面描画の復元
frmTarget.Painting = True
Application.Echo True
Exit Sub
ErrorHandler:
‘ 異常系ハンドリング:例外発生時も画面描画とメモリ解放を保証
MsgBox “グリッド操作中に予期せぬエラーが発生しました。” & vbCrLf & _
“Error: ” & Err.Description, vbCritical, “System Architecture Error”
Resume Cleanup
End Sub
—
3. シニアアーキテクトが解説するコードの急所
上記のコードには、レガシーシステムでありがちな「画面のちらつき」「メモリ肥大化」「行ズレ」を防ぐための高度な設計思想が組み込まれている。
① `RecordsetClone` の絶対性
フォームの `Recordset` を直接操作するのではなく、必ず `.RecordsetClone` を経由している点に注目してほしい。
フォームのレコードセットとクローンはデータソースを共有しているが、カレントレコードの位置(Pointer)は完全に独立している。これにより、バックグラウンドでのデータ走査や位置特定が、ユーザーの視覚的コンテキストを汚さずに安全に行える。
② `Application.Echo` と `Form.Painting` の二段構え
Access VBAにおいて、データグリッドの `Requery` は重い処理である。特にネットワーク越しのSQL Server(ADPやACCDBからのODBC接続)では、描画コストがパフォーマンスを大きく左右する。
`Application.Echo False` でAccess全体の描画を止め、さらに個別の `Form.Painting = False` でコントロールの再描画を封じる。この二段構えにより、ユーザーに「画面がガタつく」というストレスを与えないプロフェッショナルな挙動を実現する。
③ 徹底的なメモリ解放(Garbage Collectionの自前管理)
VBAのランタイムは世代が進んでも、COMオブジェクトの参照カウント管理においては依然として脆弱である。
`Set rst = Nothing` を怠ると、Accessを長時間起動し続けた際にメモリリーク(Memory Leak)を引き起こし、最終的に「リソース不足」エラーで強制終了する。
例外発生時(`ErrorHandler`)であっても必ず解放処理を通る構造にしているのは、24時間稼働を求められる社内基幹システムでは「絶対の常識」である。
—
4. マルチユーザー環境(排他制御)におけるBookmarkの罠
最後に、システム間連携や多人数同時アクセス環境において `Bookmark` を扱う際の、最上位の警告を記しておく。
DAOの `Bookmark` は、「現在のトランザクションおよびレコードセットのライフサイクル内」においてのみ有効なバイナリ値である。
例えば、以下のようなケースでは `Bookmark` は無効化され、ランタイムエラー(実行時エラー 3158: 「レコードがほかのユーザーによって変更されています」など)を引き起こす。
1. 他のユーザーが該当レコードを削除した。
2. サーバー側でインデックスが再構築され、レコードセットの物理順序が変わった(※正確にはBookmarkは物理順序に依存しないが、動的クエリの再評価でレコードセット自体が破棄された場合)。
したがって、長時間の非同期処理や、別のコネクションを挟むようなアーキテクト構造の間に `Bookmark` を持ち越してはならない。
あくまで、「同一プロシージャ内の同期的な `Requery` の前後」という、極めて限定されたスコープの中で使い倒すこと。これが、Access VBAという枯れた技術の寿命を延ばし、システムを極限まで安定させる唯一の王道である。
