「RecordsetClone」と「Bookmark」を制する:Access VBAでサブフォーム同期を完璧に制御する設計思想
Access VBAで業務ツールを開発している者が必ず一度はぶつかる壁がある。それが「メインフォームとサブフォーム、あるいは複数のフォーム間でのレコード同期」だ。
初心者はここで、`DoCmd.GoToRecord` や `Filter` プロパティを多用し、画面の激しいチラつき(フリッカー)と、予期せぬデータ更新エラーに悩まされる。あるいは、ADOで別途クエリを投げてIDで再検索し、パフォーマンスを著しく低下させる。
断言しよう。フォーム上のレコード制御に `Filter` や `Requery` を乱用するのは三流の設計だ。
プロが選ぶ最適解は、DAOの `RecordsetClone` と `Bookmark` の組み合わせである。なぜこの手法が「極限の正解」なのか。そのアーキテクチャ的な理由と、堅牢な実装パターンを伝授する。
1. アーキテクチャの本質:なぜ RecordsetClone なのか?
まず、フォームの `Recordset` と `RecordsetClone` の違いを理解せよ。
通常、フォームがデータを表示しているとき、そこには「現在のレコードポインタ」が存在する。もし、コードから直接 `Me.Recordset.FindFirst` を実行してレコードを移動させると、ユーザーが操作中のカーソルが強制的に移動し、イベント(`On Current` など)が連鎖的に発生する。 これが、無限ループや意図しない挙動、UIのガタつきを招く原因だ。
RecordsetClone という「影のコピー」
`RecordsetClone` は、フォームが保持しているレコードセットの「ポインタだけのコピー」である。
- 実データは共有しているため、メモリ消費は極めて少ない。
- ポインタ(現在位置)は独立している。 つまり、クローン側でどれだけレコードを検索して移動させても、ユーザーに見えているフォーム上の選択行は一切動かない。
この「影のコピー」で目的のレコードを特定し、その「しおり(Bookmark)」だけを本尊(フォーム)に渡す。これが、UIを一切乱さずにレコードを同期させる唯一のスマートな方法だ。
2. 実装の極意:ブックマーク同期のメカニズム
同期のフローは極めてシンプルに設計すべきだ。
1. クローンを作成する (`Me.RecordsetClone`)
2. クローン側で検索をかける (`.FindFirst`)
3. 見つかった場合、その `Bookmark` をフォームの `Bookmark` に代入する
これにより、フォームは内部的に「あぁ、このレコードの位置にジャンプすればいいのだな」と理解し、瞬時に表示を切り替える。
プロダクションレベルの実装コード
以下に、メインフォームとサブフォームの間でレコードを完璧に同期させるための、再利用可能な実装例を示す。
Option Compare Database
Option Explicit
' ======================================================================================
' 目的:サブフォームで選択したレコードをメインフォームに同期させ、
' あるいはその逆を制御するための堅牢な同期ロジック
' ======================================================================================
''' <summary>
''' 指定したフォームのレコードを、特定の条件で同期させる
''' </summary>
''' <param name="targetForm">同期させたい対象のフォームオブジェクト</param>
''' <param name="criteria">FindFirstに渡す検索条件(例: "CustomerID = 123")</param>
Public Sub SyncRecordPointer(ByRef targetForm As Form, ByVal criteria As String)
On Error GoTo Err_Handler
Dim rs As DAO.Recordset
' 1. フォームのレコードセットのクローンを取得
' これにより、UI側のカーソルを動かさずに検索が可能になる
Set rs = targetForm.RecordsetClone
' 2. クローン内で条件に合致するレコードを検索
rs.FindFirst criteria
' 3. レコードが見つかった場合のみ、ブックマークを同期
If Not rs.NoMatch Then
' ここが極意:クローンのしおりをフォーム本体にコピーする
' これにより、最小限のコストで表示レコードが切り替わる
targetForm.Bookmark = rs.Bookmark
Else
' 見つからなかった場合のハンドリング(業務仕様に合わせて実装)
Debug.Print "指定されたレコードが見つかりませんでした: " & criteria
End If
CleanUp:
' オブジェクトのライフサイクル管理を徹底せよ
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Exit Sub
Err_Handler:
MsgBox "レコード同期エラー: " & Err.Description, vbCritical
Resume CleanUp
End Sub
' --------------------------------------------------------------------------------------
' 実装例:サブフォームのレコード移動時にメインフォームを同期させる
' --------------------------------------------------------------------------------------
Private Sub Form_Current()
' サブフォーム側でこのイベントを実装し、メインフォームを同期させる
' 例:メインフォームが「frmMain」で、共通キーが「OrderID」の場合
Dim strCriteria As String
strCriteria = "OrderID = " & Me.OrderID
' メインフォームに対して同期処理を実行
' ※呼び出し元がサブフォームであるため、親フォーム(Parent)を指定
Call SyncRecordPointer(Me.Parent, strCriteria)
End Sub
3. チーフアーキテクトからの設計上の注意点
このコードを単にコピペすれば動くが、現場で「バグを出さない」ためには以下の3点を意識せよ。
① データ型の整合性に厳格であれ
`FindFirst` に渡す文字列(`criteria`)の構築ミスは、Access VBAで最も多いバグの一つだ。
- 数値型の場合: `”ID = ” & Me.ID`
- 文字列型の場合: `”Name = ‘” & Me.Name & “‘”` (シングルクォートで囲む)
- 日付型の場合: `”Date = #” & Format(Me.Date, “yyyy-mm-dd”) & “#”` (シャープで囲む)
ここを曖昧にすると、実行時エラーではなく「レコードが見つからない」というサイレントバグになり、調査コストを増大させる。
② 「Dirty」状態の罠を回避せよ
レコードを同期させる際、現在編集中のレコードがある(`Me.Dirty = True`)状態でポインタを移動させると、Accessは自動的にその変更を保存しようとする。
意図しない保存を防ぎたい場合は、同期前に `If Me.Dirty Then Me.Undo` または `Me.Dirty = False` を明示的に制御せよ。
③ パフォーマンスの重みを理解せよ
`RecordsetClone` は高速だが、`FindFirst` はインデックスが貼られていないフィールドに対して実行すると、レコード数が増えた際に目に見えて遅くなる。
同期に使用するキー(ID等)には、必ずテーブル設計段階で「インデックス(重複なし)」を設定しておくこと。 これができないエンジニアは、コードではなく設計で負けている。
まとめ:支配せよ、ポインタを。
`Filter` でレコードを絞り込むのは、データの「抽出」である。
`Bookmark` でレコードを移動させるのは、データの「参照」である。
UI操作における同期は、あくまで「参照」の切り替えで完結させるべきだ。`RecordsetClone` を使いこなし、ユーザーにストレスを与えない、静寂で高速なインターフェースを構築せよ。それが、プロフェッショナルな業務アプリケーションの証である。
