【実務・中級編】DAO.RecordsetのCloneとBookmarkを活用した、フォームの表示位置を維持したデータ更新 – Access VBA解析バイブル

スポンサーリンク

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コードこそが、長年現場で愛され、エラーを起こさず、圧倒的な生産性を生み出すシステムを支えるのです。ぜひあなたのプロジェクトの共通モジュールへ組み込み、そのパフォーマンスを体感してください。

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