【テクニカル・上級編】【中級】DAO.Recordsetの「Clone」と「Bookmark」で、フォームの表示位置を維持したままデータ更新を行う – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:DAO.Recordsetの「Clone」と「Bookmark」で、フォームの表示位置を維持したままデータ更新を行う

長年Access VBAの最前線に立ち、幾多のレガシーシステムを構築し、あるいは蘇生させてきた諸兄ならば、一度は直面したことがあるだろう。フォームの基になるレコードセットを操作し、データを更新した途端、ユーザーが苦心して探し当てた「現在選択中のレコード」が失われ、表示位置が先頭に戻ってしまう、というあの悪夢に。

これは単なるユーザーインターフェース上の不便ではない。データ入力の連続性、ユーザーの思考の流れを寸断し、業務効率を著しく低下させる深刻な問題だ。そして、この問題を安易なリフレッシュやリクエリで解決しようとすれば、更なるパフォーマンスの低下や予期せぬ挙動を引き起こすこともある。

本稿では、我々が長年培ってきたAccess VBAにおけるオブジェクトライフサイクル管理、パフォーマンスチューニング、そしてレガシー環境の保守という極限の知見をもって、この根深い問題を「DAO.RecordsetのCloneとBookmark」という、VBAの根幹をなす概念を深く掘り下げて解決する道筋を示す。単なるテクニックではない、Accessの魂を理解するための本質的なアプローチだ。

フォームにおけるレコード位置維持の難しさ:なぜRequeryだけでは不十分なのか

Accessフォームは、その性質上、常に特定のレコードセットにバインドされている。`Me.Recordset` プロパティが指し示すのは、まさにそのフォームが表示しているレコードの集合体だ。データソースがテーブルであれクエリであれ、フォームはデータの「ビュー」として機能する。

ここで問題となるのが、データ更新後の挙動である。もしフォームの基になるデータソース自体が変更された場合(例えば、関連テーブルのレコードが追加・削除された、あるいは現在のレコード以外のフィールドが更新されたなど)、フォームの内容を最新の状態に保つためには `Me.Requery` メソッドを呼び出すのが一般的だ。

しかし、この `Requery` は、フォームが保持するレコードセットを完全に再構築する操作である。つまり、内部的には既存のレコードセットを破棄し、データソースに対して新たなクエリを発行し、その結果を基に新しいレコードセットを生成しているのだ。この過程で、当然ながらフォームが以前保持していた「現在のレコード」の位置情報は失われてしまう。結果として、フォームはレコードセットの最初のレコード、あるいはソート順の最初のレコードに移動してしまう。

これは、特に大規模なレコードセットを扱う場合や、ネットワーク越しにデータベースに接続している場合、パフォーマンス上の大きなオーバーヘッドとなる。レコードセットの再構築には、データの取得、クライアント側でのフィルタリング・ソート、そしてフォームコントロールへの再バインドという一連の処理が伴うため、決して軽微な操作ではない。

我々が求めるのは、フォームの表示位置を「維持したまま」、データのみを最新の状態にすることである。この要件を満たすために、DAO (Data Access Objects) の真髄に迫る必要がある。

DAO.Recordset.Cloneの真髄とBookmarkの活用

ここで登場するのが、`DAO.Recordset.Clone` メソッドと `Bookmark` プロパティである。これらは単なるデータ操作のための機能ではなく、Accessが内部的にどのようにデータを管理し、オブジェクトの参照を扱うかという、まさにその本質を理解するための鍵となる。

Cloneメソッドの「浅いコピー」の美学

`DAO.Recordset.Clone` メソッドは、既存の `Recordset` オブジェクトの完全なコピーを生成するわけではない。むしろ、それは「浅いコピー」であり、元のレコードセットと同じ基盤となるデータキャッシュを共有する新しい `Recordset` オブジェクトを作成する。

‘ 擬似コードでCloneの本質を理解する
Dim rsOriginal As DAO.Recordset
Dim rsClone As DAO.Recordset

Set rsOriginal = CurrentDb.OpenRecordset(“SELECT FROM T_Customers”, dbOpenDynaset)
Set rsClone = rsOriginal.Clone

‘ この時点で、rsOriginalとrsCloneは異なるオブジェクト変数に格納されているが、
‘ 参照しているデータは同じメモリ領域にある。
‘ これにより、メモリ消費を抑えつつ、独立したカレントレコードポインタを持つ
‘ レコードセットを複数生成できる。

この「データキャッシュの共有」という特性が極めて重要だ。通常の `Recordset` 生成は、データソースへのクエリ発行、結果セットの取得、メモリへの展開という一連のプロセスを伴うため、非常にコストが高い。しかし `Clone` は、既存のメモリ上のデータキャッシュを再利用するため、生成コストが圧倒的に低い。

さらに重要なのは、`Clone` によって生成されたレコードセットは、元のレコードセットとは独立した「カレントレコードポインタ」を持つという点である。これにより、元のフォームが表示しているレコード(`Me.Recordset`)の位置を動かすことなく、別のレコードセット(`Me.Recordset.Clone`)を使ってデータの検索や操作を行うことが可能になるのだ。これは、マルチスレッドプログラミングにおけるスレッドローカルストレージの概念にも通じる、独立した状態管理の実現である。

Bookmarkプロパティ:レコードを一意に識別する究極のポインタ

`Bookmark` プロパティは、`Recordset` 内の特定のレコードを一意に識別するための、まさに「しおり」である。その実体は、内部的にレコードの物理アドレスや論理的な識別子を保持する `Variant` 型(`Byte` 配列)の値である。

‘ Bookmarkの内部表現を垣間見る(実際にはVBAから直接操作しない)
Dim vBookmark As Variant
vBookmark = Me.Recordset.Bookmark ‘ フォームの現在のレコードのブックマークを取得

‘ vBookmarkは例えば以下のようなバイト配列になっている可能性がある
‘ &H00 &H00 &H00 &H01 &H00 &H00 &H00 &H00 … (実際の値はRDBMSやDAOの実装による)

この `Bookmark` が強力なのは、レコードセットが `Requery` された後であっても、同じデータソースから再構築されたレコードセットに対して、以前のBookmarkを適用できるという点にある。ただし、これはレコード自体が削除されていなければの話だ。レコードの物理的・論理的な識別子が、再構築後も変わらないという前提に立っている。

つまり、フォームの表示位置を維持する戦略は以下のようになる。
1. 現在のフォームのレコードセットから `Bookmark` を取得し保存する。
2. データ更新処理を行う。
3. フォームのレコードセットを `Requery` する(最新の状態に更新)。
4. 保存しておいた `Bookmark` をフォームのレコードセットに適用し、元の位置に戻す。

この一連のプロセスにおいて、`Me.Recordset` の `Clone` を使うことで、元のフォームの表示を中断することなく、現在のレコードを指し示す `Bookmark` を安全に取得できるという利点がある。

実践!フォームの表示位置を維持したままデータ更新を行う

それでは、実際のコード例を通じて、この「CloneとBookmarkによる表示位置維持」の真髄を体験しよう。ここでは、フォーム上のデータを更新し、その結果をフォームに反映させつつ、表示位置を維持するシナリオを想定する。

シナリオ:顧客マスタフォームで顧客情報を更新し、元のレコードに留まる

顧客情報を表示するフォーム `F_顧客情報` があるとする。このフォームで特定の顧客の住所を更新した際、更新処理を実行してもフォームの表示位置が先頭に戻らず、更新した顧客のレコードに留まりたい。

‘ 標準モジュールまたはフォームモジュールの先頭に記述
Option Compare Database
Option Explicit

‘ Windows APIの宣言:メモリ操作は極力避けるべきだが、知見として
‘ Private Declare Sub CopyMemory Lib “kernel32” Alias “RtlMoveMemory” (Destination As Any, Source As Any, ByVal Length As Long)

‘///////////////////////////////////////////////////////////////////////////////////////////////////
‘ 関数名: UpdateCustomerAndMaintainPosition
‘ 概要: 顧客情報を更新し、フォームの表示位置を維持するプロシージャ
‘ 引数:
‘ lngCustomerID: 更新対象の顧客ID
‘ strNewAddress: 新しい住所
‘ 戻り値: なし
‘///////////////////////////////////////////////////////////////////////////////////////////////////
Public Sub UpdateCustomerAndMaintainPosition(ByVal lngCustomerID As Long, ByVal strNewAddress As String)
On Error GoTo Err_Handler

Dim frm As Form
Dim rsOriginal As DAO.Recordset
Dim rsCloned As DAO.Recordset
Dim vBookmark As Variant ‘ 現在のレコードのブックマークを格納

‘ 現在アクティブなフォームがF_顧客情報であることを確認
If Not Screen.ActiveForm Is Nothing Then
If Screen.ActiveForm.Name = “F_顧客情報” Then
Set frm = Screen.ActiveForm
Else
MsgBox “このプロシージャは ‘F_顧客情報’ フォームからのみ実行可能です。”, vbExclamation
Exit Sub
End If
Else
MsgBox “アクティブなフォームがありません。”, vbExclamation
Exit Sub
End If

‘———————————————————————————————–
‘ 1. 現在のレコードのブックマークを保存
‘———————————————————————————————–
‘ Me.Recordset.Bookmark を直接扱うのではなく、Cloneしたレコードセットを介して取得することで、
‘ フォームの表示状態を一時的にでも変更せず、より安全にブックマークを取得する。
‘ これは、フォームのレコードセットが一時的にDirty状態である場合など、
‘ 予期せぬ副作用を避けるための防御的なプログラミングスタイルである。
Set rsCloned = frm.Recordset.Clone
vBookmark = rsCloned.Bookmark ‘ 現在のレコードのブックマークを取得

‘———————————————————————————————–
‘ 2. データの更新処理(直接DAO.Recordsetを操作)
‘———————————————————————————————–
Dim db As DAO.Database
Dim rsUpdate As DAO.Recordset
Set db = CurrentDb

‘ 更新対象のレコードセットを開く
‘ dbOpenDynasetは更新可能なレコードセットを開く
Set rsUpdate = db.OpenRecordset(“SELECT FROM T_顧客マスタ WHERE 顧客ID = ” & lngCustomerID, dbOpenDynaset, dbAppendOnly)

If Not rsUpdate.EOF Then
rsUpdate.Edit ‘ レコードを編集モードにする
rsUpdate!住所 = strNewAddress ‘ 住所フィールドを更新
rsUpdate.Update ‘ 更新をコミット
Debug.Print “顧客ID: ” & lngCustomerID & ” の住所を ‘” & strNewAddress & “‘ に更新しました。”
Else
MsgBox “指定された顧客IDのレコードが見つかりませんでした。”, vbExclamation
End If

‘———————————————————————————————–
‘ 3. フォームのレコードセットを最新の状態に更新
‘———————————————————————————————–
‘ ここでRequeryすることで、フォームのデータが最新の状態になる。
‘ この操作により、現在のレコード位置は失われる。
frm.Requery

‘———————————————————————————————–
‘ 4. 保存しておいたブックマークを適用し、元の位置に戻す
‘———————————————————————————————–
‘ Requery後にレコードセットが空でないことを確認
If Not frm.Recordset.EOF And Not frm.Recordset.BOF Then
‘ ブックマークが有効かどうかをチェックする(重要!)
‘ 削除されたレコードのブックマークを適用しようとするとエラーになるため。
‘ BookmarkプロパティはVariant型なので、IsNullでチェック可能だが、
‘ Requery後にそのブックマークに対応するレコードが存在するかは別途確認が必要。
‘ ここでは、シンプルにRecordsetが空でないことをチェックしている。
‘ より堅牢にするなら、rsCloned.FindFirstなどで存在を確認すべき。

frm.Recordset.Bookmark = vBookmark ‘ 保存したブックマークを適用
Debug.Print “フォームの表示位置を元のレコードに戻しました。”
Else
MsgBox “フォームのレコードセットが空のため、元の位置に戻せませんでした。”, vbInformation
End If

Exit_Proc:
‘———————————————————————————————–
‘ オブジェクトの明示的解放(メモリ最適化とリソース管理の鉄則)
‘———————————————————————————————–
If Not rsUpdate Is Nothing Then
If rsUpdate.State = adStateOpen Then rsUpdate.Close ‘ ADOの場合
rsUpdate.Close ‘ DAOの場合
Set rsUpdate = Nothing
End If
If Not db Is Nothing Then Set db = Nothing
If Not rsCloned Is Nothing Then Set rsCloned = Nothing
If Not rsOriginal Is Nothing Then Set rsOriginal = Nothing ‘ 不要だが念のため
Set frm = Nothing ‘ フォームオブジェクトも解放

Exit Sub

Err_Handler:
MsgBox “エラーが発生しました: ” & Err.Description & ” (エラー番号: ” & Err.Number & “)”, vbCritical
Resume Exit_Proc
End Sub

‘———————————————————————————————–
‘ 使用例 (フォームのボタンクリックイベントなどから呼び出す)
‘———————————————————————————————–
Private Sub cmdUpdateAddress_Click()
‘ 現在のフォームの顧客IDと更新したい新しい住所を取得
Dim lngCurrentCustomerID As Long
Dim strNewAddress As String

‘ 例: 現在のフォームの[顧客ID]コントロールと[住所]コントロールから値を取得
If Not IsNull(Me!顧客ID) Then
lngCurrentCustomerID = Me!顧客ID
strNewAddress = InputBox(“新しい住所を入力してください:”, “住所更新”, Me!住所) ‘ 現在の住所を初期値に

If Len(strNewAddress) > 0 Then
Call UpdateCustomerAndMaintainPosition(lngCurrentCustomerID, strNewAddress)
Else
MsgBox “住所が入力されていません。”, vbExclamation
End If
Else
MsgBox “現在のレコードに顧客IDがありません。”, vbExclamation
End If
End Sub

コード詳細解説:オブジェクトのライフサイクルとパフォーマンスの重み

1. `Dim rsCloned As DAO.Recordset` と `vBookmark As Variant`:

  • `rsCloned` は `frm.Recordset.Clone` の結果を保持する。前述の通り、これは元のレコードセットと同じデータキャッシュを共有するが、独立したカレントレコードポインタを持つ。このため、`frm.Recordset` が参照しているレコードの位置に影響を与えることなく、安全にブックマークを取得できる。
  • `vBookmark` は `Variant` 型で宣言するのが適切である。`Bookmark` プロパティは内部的にバイト配列を返すため、`String` や `Long` では正しく扱えない。`Variant` がその柔軟性をもって、任意のデータ型(この場合はバイト配列)を受け入れる。

2. `Set rsCloned = frm.Recordset.Clone`:

  • この一行が、パフォーマンスと安定性の鍵を握る。既存のデータキャッシュを再利用するため、新たなデータベースアクセスや大量のメモリ割り当てが発生しない。極めて高速な操作である。
  • `vBookmark = rsCloned.Bookmark`:`frm.Recordset` ではなく `rsCloned` からブックマークを取得している点に注目。これは、万が一 `frm.Recordset` がDirty状態(未保存の変更がある状態)であったとしても、`Clone` がその時点での「保存された」レコードを基にブックマークを生成するため、より堅牢な取得方法となる。

3. データ更新処理 (`rsUpdate` を使用):

  • `db.OpenRecordset(…)` で直接データベーステーブルを開き、レコードを更新している。これにより、フォームのレコードセットとは独立してデータソースを操作できる。`dbAppendOnly` オプションは、レコードの追加のみを許可するが、ここでは `Edit`/`Update` 操作のために `dbOpenDynaset` と併用して `dbEdit` とするのが一般的。
  • 訂正: `dbAppendOnly` は追加専用モードなので、既存レコードの更新には不適切。`dbOpenDynaset` と `dbEdit` オプションを組み合わせるべき。

‘ 訂正後のコード(更新にはdbOpenDynasetとdbSeeChangesまたはdbDenyWriteを組み合わせる)
‘ dbOpenDynasetはデフォルトで更新可能だが、明示的にdbSeeChangesを追加することで他のユーザーの変更も反映
Set rsUpdate = db.OpenRecordset(“T_顧客マスタ”, dbOpenDynaset, dbSeeChanges)
‘ または、より限定的に顧客IDでフィルタリング
Set rsUpdate = db.OpenRecordset(“SELECT FROM T_顧客マスタ WHERE 顧客ID = ” & lngCustomerID, dbOpenDynaset, dbSeeChanges)
If Not rsUpdate.EOF Then
rsUpdate.FindFirst “顧客ID = ” & lngCustomerID ‘ 特定のレコードを探す
If Not rsUpdate.NoMatch Then
rsUpdate.Edit
rsUpdate!住所 = strNewAddress
rsUpdate.Update
End If
End If

  • この部分を `CurrentDb.Execute “UPDATE T_顧客マスタ SET 住所 = …”` のようにSQLステートメントで直接実行することも可能だ。その方がパフォーマンスは高い場合が多い。しかし、ここではDAO.Recordsetのライフサイクルを理解するために、敢えてRecordset経由での更新を示した。

4. `frm.Requery`:

  • フォームのレコードセットを完全に再構築し、最新のデータ状態を反映させる。この時点で、フォームの表示位置は失われる。しかし、我々は既に `vBookmark` でその位置を保存している。

5. `frm.Recordset.Bookmark = vBookmark`:

  • 保存しておいた `vBookmark` をフォームのレコードセットに適用する。これにより、フォームは魔法のように、更新前のレコード位置に戻る。
  • 堅牢性: Requery後に元のレコードが削除されていた場合、この行でエラーが発生する可能性がある。より堅牢なシステムでは、`Requery` 後に `frm.Recordset.FindFirst “顧客ID = ” & lngCurrentCustomerID` のようにして、更新したレコードが存在することを確認し、存在しない場合は適切なメッセージを表示するなどのフォールバック処理を実装すべきである。

6. オブジェクトの明示的解放 (`Set … = Nothing`):

  • VBAにおけるメモリ管理の最重要原則。`Recordset` や `Database` オブジェクトは、使用後に必ず `Close` メソッドを呼び出し、`Set … = Nothing` でオブジェクト参照を解放しなければならない。これを怠ると、メモリリーク、ファイルハンドルのロック、データベース接続の枯渇など、システム全体の不安定化につながる。特に、レガシー環境でAccessアプリケーションが長時間稼働する場合、この徹底がシステムの寿命を左右する。

極限の知見:パフォーマンス、メモリ、そしてレガシーの神髄

Windows APIとBookmarkの可能性(そして危険性)

`Bookmark` は `Variant` 型のバイト配列であると述べた。理論上、Windows APIの `CopyMemory` 関数(`RtlMoveMemory`)を使って、このバイト配列の中身を直接操作することも可能だ。例えば、複数のブックマークを結合したり、特定のバイト列を解析してレコードの物理アドレスを推定したりといった、低レベルな操作である。

‘ 警告: これは非常に危険な操作であり、通常は推奨されない。
‘ デバッグが極めて困難で、Accessの内部実装に依存するため、将来のバージョンで動作しなくなる可能性がある。
‘ 知見としてのみ提示する。
Private Declare Sub CopyMemory Lib “kernel32” Alias “RtlMoveMemory” (Destination As Any, Source As Any, ByVal Length As Long)

Sub ManipulateBookmarkDirectly(vBookmark As Variant)
Dim bArr() As Byte
Dim lLength As Long

If VarType(vBookmark) And vbArray Then ‘ Variantがバイト配列であるか確認
bArr = vBookmark
lLength = UBound(bArr) – LBound(bArr) + 1

‘ 例: 最初の4バイトを0クリアする (意味は保証されない)
Dim i As Long
For i = 0 To 3
bArr(i) = 0
Next i

‘ 更新したバイト配列を元のVariantに戻す
vBookmark = bArr
End If
End Sub

このようなAPI連携は、極限のパフォーマンス追求や、Accessの標準機能では不可能な特殊なデータ操作が必要な場合に検討される。しかし、それは同時に「VBAの聖域」を侵す行為であり、デバッグの困難さ、将来の互換性の喪失、そしてシステム全体の不安定化という甚大なリスクを伴う。レガシー環境の保守においては、このような低レベルな操作は、最後の手段として、かつ十分なテストとドキュメントを残して行うべきである。

メモリ最適化とオブジェクト参照の重要性

`Recordset` オブジェクトの生成は、決して軽い操作ではない。データソースへの接続、クエリの実行、結果セットのフェッチ、メモリへの展開といった一連のプロセスは、多くのシステムリソースを消費する。

  • `Clone` の真価: `Clone` メソッドは、このオーバーヘッドを最小限に抑えるための極めて有効な手段だ。既存のデータキャッシュを共有することで、新たなデータフェッチを回避し、メモリ消費も効率的になる。
  • 明示的解放の徹底: `Set rs = Nothing` の徹底は、メモリリークを防ぎ、ガベージコレクションを促すだけでなく、データベースへの接続数を適切に管理し、サーバー側のリソース枯渇を防ぐ上でも不可欠だ。特にマルチユーザー環境では、接続数が上限に達すると、新たな接続が拒否され、システムが停止する事態に陥る。

レガシー環境保守の神髄

  • DAO 3.6とACEDAO: 本稿で扱ったDAOは、Access 2000以降、特にAccess 2007以降で導入されたACCDB形式ではACEDAO (Microsoft Office Access Database Engine Object Library) と呼ばれるCOMライブラリとして提供されている。基本的なオブジェクトモデルは共通だが、細かな挙動やプロパティの差異が存在する。レガシー環境(MDB形式、Access 97/2000/2003)を保守する際には、参照設定のバージョンの違い(Microsoft DAO 3.6 Object Libraryなど)に注意し、ターゲット環境で必ず動作検証を行う必要がある。
  • ADOとの共存: 現代のAccess VBA開発では、DAOとADO (ActiveX Data Objects) が混在することが少なくない。`CurrentDb` はDAOオブジェクトであり、DAOの `Recordset` は `dbOpenDynaset` や `dbOpenSnapshot` のような `RecordsetTypeEnum` を使用する。一方、ADOの `Recordset` は `adOpenDynamic` や `adOpenStatic` のような `CursorTypeEnum` を使用し、カーソル位置の維持には `adUseClient` や `adUseServer` のカーソルロケーション設定が重要となる。両者の特性を理解し、適切な場面で使い分けることが、システム全体の安定性とパフォーマンスを最大化する鍵である。
  • マルチユーザー環境と排他制御: `Bookmark` を使ってレコード位置を維持する際、他のユーザーがそのレコードを削除していた場合、`Bookmark` は無効となる。これは、楽観的ロック(更新時に排他チェックを行う)や悲観的ロック(レコードをロックして他のユーザーの操作を制限する)といった排他制御の設計と密接に関連する。アプリケーション設計の段階で、データ競合の可能性を十分に考慮し、適切なエラーハンドリングとユーザーへのフィードバック機構を組み込むべきである。

システム間連携におけるBookmarkの扱い

Accessフォームが外部アプリケーション(例えばVB.NET製のクライアントアプリケーションやExcel VBA)から操作される場合、`Application.Run` メソッドなどでAccessのVBAプロシージャを呼び出すことがある。この際、Accessフォームの `Bookmark` を外部アプリケーションに渡し、再度Accessに戻すことで、複雑な連携処理の後もフォームの表示位置を維持することが可能になる。

ただし、`Bookmark` の実体がバイト配列であるため、外部アプリケーションとのデータ型のマッピングに注意が必要だ。VB.NETであれば `Byte()` 型として扱うことになる。COMインタフェースを介したデータ転送では、`Variant` 型のマーシャリングが正しく行われるかどうかの検証も欠かせない。

結論:単なるテクニックではない、Accessの設計思想

`DAO.Recordset.Clone` と `Bookmark` を用いたフォームの表示位置維持は、単なる小手先のテクニックではない。これは、Accessのオブジェクトモデルが持つ「データとビューの分離」「効率的なリソース管理」「ユーザーエクスペリエンスの維持」という設計思想の具現化である。

我々がVBAというレガシー言語を扱い続ける中で、表層的なコードの羅列に終始するのではなく、その背後にあるオブジェクトのライフサイクル、メモリ上の振る舞い、そしてデータベースとの対話の本質を理解することが、真に堅牢で高性能なシステムを構築し、長期間にわたって保守していくための絶対条件だ。

クラウド化やWeb化が進む現代においても、Accessのようなデスクトップデータベースアプリケーションは、その即応性と柔軟性から、依然として多くの企業の基幹業務を支えている。この古くて新しい技術の真髄を掌握することは、単に現状を維持するだけでなく、未来のシステムへと繋がる知見の礎となるだろう。我々チーフアーキテクトが共有すべきは、まさにこの「極限の知見」なのだ。

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