【実務・中級編】DAO.Recordsetの「Bookmark」プロパティを駆使した複雑なグリッド操作の実装 – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:DAO.Recordsetの「Bookmark」を駆使したグリッド位置完全復元術

こんにちは。大規模な業務システムから局所的な自動化ツールまで、数々のAccess案件をアーキテクトとして率いてきた。

Accessの開発現場で、最も頻繁に遭遇し、かつ雑に実装されてユーザーのヘイトを最も買いやすいポイントがどこか知っているか?
それは「サブフォームやデータシートでのデータ処理後、表示位置(カレントレコード)が勝手に先頭や末尾に吹っ飛ぶ現象」だ。

例えば、ボタン一つで選択された複数レコードのステータスを一括更新する処理を作ったとする。
処理が終わった瞬間、画面がババッとスクロールし、ユーザーが苦労して探して選択していた行が視界から消え去る。ユーザーは「おい、さっきの行はどこに行った?」とイライラしながらスクロールバーをまさぐる。

この絶望的なUI体験を根絶するのが、今回解説する `DAO.Recordset` の `Bookmark` プロパティ を利用した位置完全復元テクニックだ。
ネット上のそこら中にある「お茶を濁したようなコード」ではなく、実務の現場で絶対にバグを起こさない、プロフェッショナルな設計と思想を伝授する。

1. なぜ「行番号」や「ID検索」ではダメなのか?(非効率な設計の否定)

未熟なプログラマーがやりがちな実装がこれだ。

  • 処理の直前に、選択されているレコードの主キー(IDなど)を記憶する。
  • 処理が終わった後、`Recordset.FindFirst “ID = ” & savedID` で探し直す。

これ、今すぐやめなさい。

理由は明確だ。
1. パフォーマンスの無駄: インデックスが貼られていなければフルスキャンになるし、ネットワーク越し(Client/Server構成)のSQL Serverバックエンドであれば無駄なラウンドトリップが発生する。
2. ソート順やフィルタの罠: ユーザーがフォーム側で並び替えやフィルターを行っている場合、IDでジャンプしても「画面上の視覚的な位置」に戻るとは限らない。
3. 新規追加・削除の矛盾: 処理の過程で該当レコードが削除されていた場合、`FindFirst` は迷子になり、エラーか予期せぬ挙動を引き起こす。

ブックマーク(Bookmark)という唯一無二の解

DAOの `Bookmark` は、データベースエンジン内部における「レコードの一意な物理位置を指し示すバイナリトークン(String型)」である。
IDがどうこう、並び順がどうこうではない。「今、目の前にあるこの行の物理アドレス」を直接掴む。だからこそ、処理の前後でこれほど正確かつノーコストに位置を復元できる手段はほかに存在しない。

2. 堅牢な実装のためのアーキテクチャ設計

サブフォームのレコードセットを操作する際、以下の鉄則を守る必要がある。

  • `CurrentDb` ではなく、フォームの `Recordset` を直接叩く

グローバルな `CurrentDb` で別インスタンスのレコードセットをいじっても、画面上のフォーム(サブフォーム)のカーソルは連動しない。必ず `Me.SubformName.Form.Recordset`(またはフォーム自体の `Me.Recordset`)を操作する。

  • ブックマークの有効性確認(IsNothing / ヌルチェック)

データが1件もない空の状態でブックマークを取得しようとすると、容赦なく実行時エラー(エラー 3021: 現在レコードがありません)が飛んでくる。例外処理または事前チェックは必須。

  • 画面描画の凍結(Echoオフ / 画面再描画の抑制)

レコード移動の際に画面がチラつくのを防ぐため、`Me.Appliation.Echo` や `SysCmd` を賢く使う。

3. 【プロダクションコード】位置を完璧に保持する一括処理プロシージャ

それでは、実務でそのまま使える堅牢なコードを提示する。
今回は、「サブフォームで選択されている(あるいはカレントになっている)レコードを起点に何らかのバッチ処理を行い、処理完了後びくともせずに元の位置・フォーカスを維持する」というシナリオだ。

‘ =========================================================================
‘ モジュール名: 業務ロジック標準モジュール / フォームクラス
‘ 処理概要: DAO.Bookmarkを活用したサブフォームの安全なレコード位置復元処理
‘ =========================================================================
Public Sub ExecuteProcessWithBookmark(frmSub As Form)
Dim rs As DAO.Recordset
Dim varBookmark As Variant

‘ 1. 対象フォームのレコードセットを取得
Set rs = frmSub.Recordset

‘ 2. レコードが存在しない、またはBOF/EOFの場合は即座に抜ける(ガード節)
If rs.EOF And rs.BOF Then
MsgBox “処理対象のデータが存在しません。”, vbExclamation, “処理中断”
Exit Sub
End If

‘ エラーハンドリングの開始(予期せぬ切断やロック競合対策)
On Error GoTo ErrorHandler

‘ 画面描画を一時停止し、処理中のチラつきと無駄な再描画コストを排除
Application.Echo False
frmSub.Painting = False

‘ 3. 現在のブックマーク(物理位置)を退避
‘ ※レコードが新規行(ファントム行)を指している場合は取得できないためトラップ
If Not (rs.EOF Or rs.BOF) Then
If Not frmSub.NewRecord Then
varBookmark = rs.Bookmark
End If
End If

‘ =====================================================================
‘ 4. メインのデータ処理(例:トランザクションを伴う一括ステータス更新)
‘ =====================================================================
CurrentDb.Execute “UPDATE T_Orders SET Status = ‘処理済’ WHERE TargetFlg = True”, dbFailOnError

‘ レコードセットの変更をフォームに即時反映(キャッシュの破棄と再同期)
rs.Requery

‘ 5. ブックマークの位置へ復元を試みる
If Not IsEmpty(varBookmark) Then
‘ ブックマークが有効か(削除されていないか)を検証しつつジャンプ
On Error Resume Next
rs.Bookmark = varBookmark
If Err.Number <> 31590 And Err.Number <> 3021 Then
‘ 31590 (無効なブックマーク) 以外のエラーなら通常通り投げる
If Err.Number <> 0 Then Err.Raise Err.Number, , Err.Description
Else
‘ 万が一、処理の過程で当該レコードが消されていた場合のフォールバック
MsgBox “元のレコードは他のユーザーによって削除されたか、移動しました。” & vbCrLf & _
“先頭行にフォーカスを合わせます。”, vbInformation, “通知”
If Not (rs.BOF And rs.EOF) Then rs.MoveFirst
End If
On Error GoTo ErrorHandler
End If

CleanUp:
‘ 6. 画面描画の復元(必ず実行する)
frmSub.Painting = True
Application.Echo True
Set rs = Nothing
Exit Sub

ErrorHandler:
‘ 異常系ハンドリング
Application.Echo True
frmSub.Painting = True
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error No: ” & Err.Number & vbCrLf & _
“Description: ” & Err.Description, vbCritical, “システムエラー”
Set rs = Nothing
End Sub

4. チーフアーキテクトからの実践的アドバイス

上記のコードを自分のシステムに組み込むにあたり、以下の2点を心に刻んでおいてほしい。

① マルチユーザー環境(排他制御)への配慮

Accessのバックエンドがファイルサーバー上の共有ACCDBである場合、他のユーザーが同じレコードを削除・変更している可能性が常にある。そのため、コード内でも行っている通り、`rs.Bookmark = varBookmark` の代入時には必ず `On Error Resume Next` でトラップし、「万が一ブックマークが無効になっていてもシステムがクラッシュしない」タフな構造にしておくことがプロの条件だ。

② フォームの `.Requery` と `Recordset` の寿命

`rs.Requery` を実行すると、DAOのレコードセットオブジェクト自体が内部的に再クエリされ、メモリ上の参照(ポインタ)が変わることがある。近年のAccess(Access 2016以降など)ではかなり安定しているが、古いバージョンや複雑なJOINクエリをバインドしているフォームでは、Requery後にブックマークが外れる挙動を示すことがある。不安な場合は、`Requery` の代わりにフォーム側のメソッド(`frmSub.Requery`)を叩き、その後に改めてレコードセットを再取得してブックマークを当てるアプローチも検討せよ。

総括

たかが「行の位置を戻す」だけの処理に、ここまでこだわる理由。
それは、「プロが作ったシステム」と「素人が作ったツール」の差は、こういう細部の神様に宿るからだ。

ユーザーは毎日何百回もそのボタンを押す。そのたびに画面がビュンと跳ねるストレスを与えるか、何事もなかったかのようにシームレスに仕事が進むか。その差が、システムの評価、ひいてはあなたのエンジニアとしての信頼を決定づける。

DAO.Bookmark。この強力なプリミティブを完全に手なずけ、ワンランク上の堅牢なAccessアプリケーションを構築してほしい。

タイトルとURLをコピーしました