Access VBAプロの常識:データ更新時にフォーム位置をミリ秒単位で完全保持する「Recordset.Clone × Bookmark」極限構成
Accessで業務アプリケーションを構築する際、ユーザーの満足度を著しく低下させる最大の要因をご存知でしょうか?
それは、「データを更新・再読込(Requery)した瞬間に、フォームのスクロール位置が最上部に巻き戻り、選択していた行を見失う」 という現象です。
アマチュアの開発者は「仕様です」と諦めるか、`DoCmd.GoToRecord` や `Me.Requery` を無造作に呼び出し、画面を派手にチラつかせながら低速なループ処理を書きます。しかし、プロフェッショナルは違います。
本記事では、Access VBAのカーソル制御の核心である `DAO.Recordset.Clone` と `Bookmark` のメカニズムを解剖し、ユーザーの視界と操作感を一切破綻させない、完璧なデータ更新アーキテクチャを伝授します。
—
1. なぜ `Me.Requery` 単体ではダメなのか?(メモリとカーソルの関係)
まず、Accessフォームの裏側で何が起きているのか、オブジェクトモデルの観点から正しく理解しましょう。
`Me.Requery` を実行すると、Accessは以下の処理を強制的に行います。
1. 既存のレコードセット(データキャッシュ)を完全に破棄する。
2. データベース(JET/ACEエンジンまたはSQL Server)に対してSQLを再発行する。
3. 取得したレコードセットの「先頭行(1行目)」にカーソル(カレントレコード)を配置する。
この挙動により、ユーザーがどれだけ下方にスクロールして特定の行を編集していても、視点(フォーカス)は問答無用でトップに吹き飛びます。これでは大量のデータを処理する実務でストレスが爆発するのは当然です。
愚策:「IDを保持して `FindFirst` で検索」の罠
「更新前の主キーIDを変数に保持し、Requery後に `FindFirst` で探し直せばいい」と考えた方は甘いと言わざるを得ません。
- 画面の強烈なチラつき(描画コスト)
- 全件スキャンによるパフォーマンス低下
- Formの各種イベント(`Current`, `BeforeUpdate` 等)が無駄に走る設計的副作用
我々が目指すべきは、「ユーザーに見えないバックグラウンドで処理を完了させ、フォームの見た目とカーソル位置をミリ秒単位で同期させる」 ことです。
—
2. 解決の鍵:`RecordsetClone` と `Bookmark` の本質
この問題をスマートに解くための武器が `RecordsetClone` と `Bookmark` です。
`RecordsetClone` とは?
フォームが現在保持しているレコードセットの「独立したカーソル(ポインタ)」を複製したものです。
- メモリの共有: データのバックエンドとの通信バッファはフォーム本体と共有しています。そのため、別途 `OpenRecordset` を実行するような重い処理は発生しません。
- カーソルの独立: フォーム上の見た目(UI)の選択行を一切動かすことなく、裏側で自由になぞったり、検索したり、データを書き換えたりできます。
`Bookmark`(ブックマーク)とは?
レコードセット内の特定の行を物理的に指し示す「バイナリデータのポインタ(メモリ直接参照)」です。
文字列でのID検索とは異なり、データエンジンが内部で管理している絶対アドレスであるため、$O(1)$ に極めて近い圧倒的な速度で対象レコードへダイレクトにジャンプできます。
> 【アーキテクトの注意書き】
> フォームの `Bookmark` と `RecordsetClone` の `Bookmark` は相互に代入(同期)が可能です。つまり、Clone側で目的のレコードに移動し、その `Bookmark` をフォームに書き戻すだけで、UIのカーソルが一瞬で目的の行に復元されます。
—
3. 実装の設計パターン(堅牢な処理フロー)
堅牢な更新処理を構築するためには、単にBookmarkを戻すだけでなく、例外処理や描画制御を含めた徹底的なプログラミングが必要です。
[処理開始]
│
├─ 1. Application.Echo False (画面の描画を停止し、チラつきを極限まで抑える)
│
├─ 2. 現在のフォームの Bookmark を退避(新規レコード等の例外チェック含む)
│
├─ 3. RecordsetClone を取得し、バックグラウンドでデータ操作 or Requery
│
├─ 4. Requery実行後、退避した Bookmark または 主キーID を元に位置を復元
│
├─ 5. フォームの Bookmark に値を書き戻す
│
[後処理]
│
└─ 6. Application.Echo True (必ず描画を再開) & オブジェクトの完全解放
—
4. プロダクション環境仕様のコード例
以下は、そのまま実務の現場でコピペして利用できる、バグ耐性を極限まで高めた実装パターンです。
状況:現在の行の位置と選択状態を保持したまま、データを再読込(Requery)して最新化する
Option Explicit
‘ ================================================================================
‘ 機能: フォームの表示位置・選択行を完璧に維持したまま Requery を実行する
‘ 引数: frm – 対象のAccessフォームオブジェクト
‘ 主キー名: TargetPKName – 復元を確実にするための主キー項目名
‘ ================================================================================
Public Sub RefreshFormKeepPosition(ByRef frm As Access.Form, Optional ByVal TargetPKName As String = “ID”)
On Error GoTo ErrorHandler
‘ 1. 編集中(Dirty)であれば、まず保存を完遂させる
If frm.Dirty Then
frm.Dirty = False
End If
‘ 2. 新規レコード位置にいる場合や、レコードが空の場合は単純にRequeryして終了
If frm.NewRecord Or frm.Recordset.RecordCount = 0 Then
frm.Requery
Exit Sub
End If
‘ 描画停止(チラつき防止)
Application.Echo False
‘ 3. 現状の情報を保持
Dim savedPKValue As Variant
savedPKValue = frm.Controls(TargetPKName).Value
‘ 4. データ再読込
frm.Requery
‘ 5. RecordsetClone を使用して高速位置復元
Dim rsClone As DAO.Recordset
Set rsClone = frm.RecordsetClone
‘ 主キーで高速検索(BookmarkはRequeryで無効化されるため主キーを照合線とする)
Dim criteria As String
If VarType(savedPKValue) = vbString Then
criteria = BuildCriteria(TargetPKName, dbText, savedPKValue)
Else
criteria = TargetPKName & ” = ” & savedPKValue
End If
rsClone.FindFirst criteria
‘ 6. 一致するレコードが存在すれば、Bookmarkを同期してUIを復元
If Not rsClone.NoMatch Then
frm.Bookmark = rsClone.Bookmark
End If
ExitProc:
‘ 【重要】オブジェクトの参照解除(メモリリーク防止)
‘ ※RecordsetCloneはCloseしてはいけない(フォームのレコードセットを破棄してしまうため)
Set rsClone = Nothing
‘ 画面描画の確実な再開
Application.Echo True
Exit Sub
ErrorHandler:
‘ エラーログ出力や適切な例外処理
MsgBox “データの更新中にエラーが発生しました。: ” & Err.Description, vbCritical, “システムエラー”
Resume ExitProc
End Sub
‘ 補足補助関数: エスケープ処理を考慮した条件文構築
Private Function BuildCriteria(ByVal fieldName As String, ByVal fieldType As Long, ByVal val As Variant) As String
‘ シングルクォーテーションのダブリング処理でSQLインジェクション/構文エラーを防ぐ
Dim cleanVal As String
cleanVal = Replace(CStr(val), “‘”, “””)
BuildCriteria = fieldName & ” = ‘” & cleanVal & “‘”
End Function
—
5. 深掘り:プロが押さえるべきライフサイクルとメモリの落とし穴
上記のコードには、一般的な解説書には書かれていない超重要なルールが隠されています。ここを理解していないと、ランタイムエラーやメモリリークの温床となります。
落とし穴1:`RecordsetClone` を `Close` してはならない
通常の `CurrentDb.OpenRecordset` で開いたオブジェクトは、使い終わったら `.Close` を呼ぶのが鉄則です。
しかし、`Me.RecordsetClone` に対して `.Close` を実行すると、フォーム本体のデータソースまで切断され、フォームが破損(#Error 表示)します。
- NG: `rsClone.Close` (絶対不可)
- OK: `Set rsClone = Nothing` (ポインタの破棄のみ行う)
落とし穴2:`Requery` 後は `Bookmark` が無効化される
同じレコードであっても、`Me.Requery` を実行した瞬間に、メモリ上のバイナリアドレス(Bookmark)は全て再構築されます。そのため、Requeryを跨いで `Bookmark` の値を直接復元することはできません。
Requeryを行う場合は、コード例のように 「主キー(PK)を退避」→「Requery」→「Clone側で FindFirst」→「ヒットした Clone.Bookmark を Form.Bookmark に代入」 というステップを踏むのが唯一絶対の正解です。
逆に、Requeryを伴わないバックグラウンドのデータ更新(`Clone` 自体を使って値を書き換える場合)であれば、`Bookmark` を直接退避・復元可能です。
状況:Requeryせず、裏でデータを書き換えてUIに即座に反映する場合
Public Sub SilentUpdateCurrentRecord(ByRef frm As Access.Form)
On Error GoTo ErrorHandler
Dim rsClone As DAO.Recordset
Set rsClone = frm.RecordsetClone
‘ フォームの現在行のBookmarkをCloneに同期
rsClone.Bookmark = frm.Bookmark
‘ バックグラウンド更新(UIイベントを一切発生させない)
rsClone.Edit
rsClone.Fields(“LastUpdated”).Value = Now()
rsClone.Fields(“UpdatedBy”).Value = Application.CurrentUser
rsClone.Update
‘ フォームの画面だけを軽くリフレッシュ(RequeryではなくRefresh)
frm.Refresh
ExitProc:
Set rsClone = Nothing
Exit Sub
ErrorHandler:
MsgBox “更新失敗: ” & Err.Description, vbExclamation
Resume ExitProc
End Sub
`Me.Refresh` はデータ構造(行数や順序)を変更せず、「現在表示されているレコードの最新の値だけを画面に再描画する」 ため、スクロール位置やフォーカスは1ミリもズレません。`RecordsetClone.Update` と `Form.Refresh` の組み合わせは、最も美しいデータ更新の黄金パターンです。
—
6. まとめ:業務アプリの品格を決める細部へのこだわり
業務システムの品質は、単に「動くか動かないか」ではなく、「ユーザーの思考を中断させないか」 で決まります。
1. `Me.Requery` を無造作に呼ばない。 目的によって `Refresh` と使い分ける。
2. 位置保持には `RecordsetClone` を活用する。 フォームのUIカーソルを汚さずに処理を完結させる。
3. `Bookmark` の同期でミリ秒の操作感を実現する。
4. `Application.Echo False/True` でプロフェッショナルなシームレス感を提供する。
5. `RecordsetClone` は絶対に `.Close` せず `Set = Nothing` で解放する。
この一見地味な「表示位置の維持」にまでこだわり抜かれたVBAコードこそが、長年現場で愛され、エラーを起こさず、圧倒的な生産性を生み出すシステムを支えるのです。ぜひあなたのプロジェクトの共通モジュールへ組み込み、そのパフォーマンスを体感してください。
