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

スポンサーリンク

【中級への扉】フォームの表示位置を維持したままデータ更新!DAO.Recordsetの「Clone」と「Bookmark」完全攻略

皆さん、こんにちは! Access VBAの世界へようこそ。

あなたは今、フォームでデータを操作していて、こんなお悩みを抱えていませんか?

「ボタンをクリックしてデータを更新すると、せっかく見ていたレコードが先頭に戻っちゃう…」
「フォームを再クエリすると、またスクロールし直さなきゃいけなくて面倒…」

まさに「あるある」の悩みですよね。私もかつて、この問題に何度も頭を抱えました。でも、もう大丈夫です!
今日は、この悩みを一発で解決するAccess VBAの強力な武器、DAO.Recordsetの「Clone」と「Bookmark」について、基礎から本質まで、とことん深掘りして解説していきます。

ここをクリアすれば、あなたもAccess VBAの基本をバッチリとマスターし、ユーザー体験を格段に向上させるプロの技を手に入れたも同然ですよ!

フォームのデータ更新、なぜ「表示位置」が迷子になるのか?

まずは、なぜこのような問題が起きるのか、そのメカニズムを少しだけ理解しておきましょう。

Accessのフォームは、その背後に「レコードソース」として指定されたテーブルやクエリからデータを読み込み、それをまるで本のページをめくるように表示しています。この「データそのもの」をVBAの世界ではレコードセット(Recordset)と呼びます。

フォーム上でデータを更新したり、削除したりすると、Accessは内部的にフォームの表示内容を最新の状態に保とうとします。最も手軽な方法の一つが、フォームの`Requery`メソッドです。これは、フォームのレコードセットを丸ごと作り直す、という処理に相当します。

まるで、本を読み途中で一度閉じて、また新しい本を開き直すようなものです。新しい本を開けば、当然「しおり」は失われ、最初から読み始めることになりますよね?これが、フォームの表示位置が失われる原因なんです。

でも、安心してください。この問題を解決する魔法の「しおり」と「同じ内容の別冊」が、Access VBAには用意されています。それが「Bookmark」と「RecordsetClone」です!

救世主登場!「RecordsetClone」と「Bookmark」とは?

1. レコードセットの「分身」を生み出す`RecordsetClone`

フォームが持っている「レコードセット(データの本)」は一つだけではありません。実は、フォームには`Recordset`プロパティ`RecordsetClone`プロパティという、似て非なる二つのレコードセットが存在します。

  • `Me.Recordset`: フォームが現在表示している、まさに「今開いている本」そのものです。このレコードセットのカーソル(ページ位置)を動かすと、フォームの表示も連動して動きます。
  • `Me.RecordsetClone`: これが今回の主役の一つ!フォームの`Recordset`と全く同じデータを指し示していますが、独立したカーソルを持つ「分身」のようなレコードセットです。

例えるなら、`Me.Recordset`が「図書館から借りてきた本」だとしたら、`Me.RecordsetClone`は「その本をコピー機で丸ごと複製した、全く同じ内容の別冊」といったイメージです。

この`RecordsetClone`を使う最大のメリットは、フォームの表示(`Me.Recordset`)に影響を与えることなく、裏側でデータの検索や操作ができる点にあります。これこそが、フォームの表示位置を維持したまま、データ更新を行うための第一歩なんです。

2. 現在地を記憶する「しおり」`Bookmark`

もう一つの主役が`Bookmark`プロパティです。これは、レコードセット内の特定のレコードを指し示す「しおり」のような役割を果たします。

一般的な「行番号」とは少し違います。`Bookmark`は、データベース内部でそのレコードを一意に識別するための、物理的なアドレスのようなものです。そのため、レコードの追加や削除があっても、一度取得した`Bookmark`が指し示すレコードが変わることはありません(ただし、レコード自体が削除された場合は別ですが)。

この`Bookmark`を、データ更新処理を行う前に保存しておき、処理後にその`Bookmark`を使って元のレコードへ戻る、というのが基本的な考え方になります。

【実践】表示位置を維持したままデータ更新を行うVBAコード

それでは、具体的なシナリオでコードを見ていきましょう。
ここでは、商品マスタフォームで、特定の条件(例えば「在庫数が100個を下回る商品」)のデータを一括更新するボタンを例に考えます。更新後に、フォームの表示位置は元のレコードのままにしたい、という要件です。

前提:DAOライブラリへの参照設定

DAO.Recordsetを使用する場合、プロジェクトに「Microsoft DAO 3.6 Object Library」またはそれ以降のバージョンへの参照設定が必要です。
VBAエディタで`ツール` > `参照設定` を開き、該当するライブラリにチェックを入れてください。

サンプルコード

Private Sub cmdUpdateInventory_Click()
‘———————————————————-
‘ フォームの表示位置を維持したまま、特定の条件で在庫データを更新する例
‘———————————————————-

‘ エラーハンドリングの開始
On Error GoTo ErrorHandler

Dim rsClone As DAO.Recordset
Dim vCurrentBookmark As Variant ‘ BookmarkはVariant型で宣言するのが安全
Dim strSQL As String ‘ 更新クエリ用のSQL文字列

‘ 1. 現在のレコードのブックマークを保存する
‘ ———————————————————-
‘ 現在フォームが表示しているレコードのBookmarkを取得します。
‘ これが、処理後にフォームを元の位置に戻すための「しおり」になります。
‘ レコードセットが空の場合や、新規レコードの場合、Bookmarkは無効なのでチェックが必要です。
If Not Me.Recordset.BOF And Not Me.Recordset.EOF Then
vCurrentBookmark = Me.Recordset.Bookmark
Else
‘ レコードが一件もない、または新規レコードの場合、ブックマークは保存しない
‘ この場合、処理後にフォームの先頭レコードなどに移動することになります。
vCurrentBookmark = Null
End If

‘ 2. RecordsetCloneを使って更新処理を行う(または、別のレコードセットを開く)
‘ ———————————————————-
‘ ここでは、フォームのRecordsetCloneを直接操作するのではなく、
‘ 更新クエリを実行する方式を採用します。
‘ 大量データを一括更新する場合、SQLのUPDATEクエリが最も効率的です。
‘ RecordsetCloneを使うのは、レコードを一件ずつ更新する場合や、
‘ 更新対象レコードを特定して操作する場合に強力です。

‘ 例: 在庫数が100未満の商品を、在庫数100に更新するSQL
‘ ご自身のテーブル名、フィールド名に合わせて修正してください。
strSQL = “UPDATE 商品マスタ ” & _
“SET 在庫数 = 100 ” & _
“WHERE 在庫数 < 100;" ' SQLを実行する ' DoCmd.RunSQLはメッセージが表示されるため、ここではCurrentDb.Executeを使用します。 ' dbFailOnErrorオプションは、エラー発生時に処理を停止させます。 CurrentDb.Execute strSQL, dbFailOnError ' 3. フォームを再クエリして最新の状態に更新する ' ---------------------------------------------------------- ' 更新処理が完了したら、フォームのデータを最新の状態にリフレッシュします。 ' これにより、更新されたデータがフォームに反映されます。 Me.Requery ' 4. 保存しておいたブックマークへ戻る ' ---------------------------------------------------------- ' フォームが再クエリされ、新しいレコードセットが作成されましたが、 ' vCurrentBookmarkには元のレコードの位置が記憶されています。 ' そのBookmarkをフォームのRecordsetに適用することで、元の表示位置に戻ります。 If Not IsNull(vCurrentBookmark) Then Me.Recordset.Bookmark = vCurrentBookmark End If ' 処理完了のメッセージ (必要に応じて) MsgBox "在庫データの更新が完了しました。", vbInformation Exit_Sub: ' 開いたレコードセットは必ずクローズし、Nothingに設定してメモリを解放します。 ' この例ではCurrentDb.Executeを使っているので、レコードセットを開いていません。 ' もしrsCloneを使っていた場合は、ここでクローズ処理が必要です。 If Not rsClone Is Nothing Then If rsClone.State = adStateOpen Then ' DAO.Recordsetの場合、Stateプロパティは存在しません。 ' 正しくは If rsClone.State = 1 Then ではなく、 ' Closeメソッドを直接呼び出すか、 ' ADOの場合のチェックです。 ' DAOの場合、OpenRecordsetで開いたものを明示的にCloseします。 ' 今回はRecordsetCloneは直接操作していませんが、 ' もしOpenRecordsetを使っていたら以下のようにします。 ' rsClone.Close End If Set rsClone = Nothing End If Exit Sub ErrorHandler: ' エラー発生時の処理 MsgBox "エラーが発生しました: " & Err.Description, vbCritical Resume Exit_Sub ' 終了処理へジャンプ End Sub

コード解説

1. `vCurrentBookmark = Me.Recordset.Bookmark`:

  • これが「しおり」を保存する処理です。`Me.Recordset.Bookmark`で、現在フォームが表示しているレコードのユニークな位置情報(Bookmark)を取得し、`vCurrentBookmark`変数に格納します。
  • ポイント: レコードセットが空の場合(`Me.Recordset.BOF`または`Me.Recordset.EOF`がTrue)、または新規レコード(まだ保存されていないレコード)の場合、Bookmarkは無効です。そのため、`If Not Me.Recordset.BOF And Not Me.Recordset.EOF Then`で有効なBookmarkが存在するかどうかをチェックしています。

2. `CurrentDb.Execute strSQL, dbFailOnError`:

  • ここでは、SQLの`UPDATE`クエリを使って、バックエンドで直接データを更新しています。この方法が、大量データを更新する際には最も効率的です。フォームのレコードセットを直接操作する必要がない場合は、この方法が推奨されます。
  • `CurrentDb.Execute`を使うことで、確認メッセージを表示せずに更新を実行できます。

3. `Me.Requery`:

  • データが更新されたら、フォームの内容を最新の情報に更新するために`Requery`メソッドを呼び出します。これにより、フォームのレコードセットが再構築され、更新されたデータが反映されます。
  • ただし、この`Requery`によって、フォームの表示位置はリセットされてしまいます。

4. `Me.Recordset.Bookmark = vCurrentBookmark`:

  • ここで、保存しておいた「しおり」の出番です!`Requery`で新しいレコードセットになったフォームの`Recordset`に対し、`vCurrentBookmark`を再設定することで、フォームの表示を元のレコードの位置に戻します。

もう一歩踏み込む!RecordsetCloneを直接使うパターン

上記の例では、`CurrentDb.Execute`を使って一括更新を行いました。では、`RecordsetClone`はどんな時に役立つのでしょうか?
例えば、「フォーム上で選択されたレコードと同じ顧客の、別のテーブルのレコードを更新したい」といった、フォームの表示レコードを起点に、関連データを一件ずつ操作したい場合に`RecordsetClone`は非常に強力です。

‘ RecordsetCloneを直接使う例(部分的なコード)

Private Sub cmdUpdateSpecificRelatedData_Click()
On Error GoTo ErrorHandler

Dim rs As DAO.Recordset
Dim vCurrentBookmark As Variant

‘ 現在のレコードのブックマークを保存
If Not Me.Recordset.BOF And Not Me.Recordset.EOF Then
vCurrentBookmark = Me.Recordset.Bookmark
Else
vCurrentBookmark = Null
MsgBox “ブックマークが取得できません。フォームにレコードがあるか確認してください。”, vbExclamation
GoTo Exit_Sub
End If

‘ RecordsetCloneをセット
‘ ここでフォームのレコードセットの分身を取得します。
Set rs = Me.RecordsetClone

‘ フォームの現在のレコードと同じIDのレコードをRecordsetCloneで検索
‘ 例えば、フォームの主キーが[商品ID]だと仮定します。
rs.FindFirst “[商品ID] = ” & Me![商品ID]
‘ FindFirstで見つかったかどうかをチェック
If Not rs.NoMatch Then
‘ 見つかったレコードを更新する例
‘ (実際には、rsで関連テーブルを開いて更新するなどの処理が入ります)
Debug.Print “RecordsetCloneで商品ID: ” & rs![商品ID] & ” のレコードを見つけました。”
‘ ここでrsを使って、他の関連データを更新する処理などを記述します。
‘ 例: 関連する在庫情報テーブルの更新など
‘ CurrentDb.Execute “UPDATE 関連テーブル SET … WHERE 関連ID = ” & rs![関連ID], dbFailOnError
Else
MsgBox “RecordsetCloneで該当レコードが見つかりませんでした。”, vbExExclamation
End If

‘ 更新処理後、フォームをリフレッシュ
Me.Requery

‘ ブックマークへ戻る
If Not IsNull(vCurrentBookmark) Then
Me.Recordset.Bookmark = vCurrentBookmark
End If

Exit_Sub:
If Not rs Is Nothing Then
‘ RecordsetCloneはフォームのライフサイクルに依存するため、
‘ 明示的なrs.Closeは不要な場合が多いですが、
‘ 安全策としてSet rs = Nothingは実行しておきましょう。
Set rs = Nothing
End If
Exit Sub

ErrorHandler:
MsgBox “エラーが発生しました: ” & Err.Description, vbCritical
Resume Exit_Sub
End Sub

この例では、`Me.RecordsetClone`を`rs`変数に格納し、その`rs`を使って`FindFirst`で特定のレコードを検索しています。`Me.RecordsetClone`はフォームの表示には影響を与えないため、フォームの表示位置はそのままに、裏で複雑な検索や操作を行うことができます。

ここが極限の知見!CloneとBookmarkの奥深さ

さて、一般的な解説はここまで。ここからは、ベテランエンジニアだからこそ知っている、CloneとBookmarkの「極限の知見」を少しだけお話ししましょう。

1. `RecordsetClone`のパフォーマンス優位性:
`CurrentDb.OpenRecordset`で新しいレコードセットを開くこともできますが、フォームのデータソースと同じ内容を操作するなら、`Me.RecordsetClone`を使う方が一般的にパフォーマンスに優れています。なぜなら、`RecordsetClone`は既存のフォームのレコードセットが持つデータキャッシュを共有できるため、一からデータを取り直すオーバーヘッドが少ないからです。オブジェクトのライフサイクルを意識し、不必要なリソースの確保と解放を避けることが、安定したシステム構築には不可欠です。

2. `Bookmark`は単なる行番号ではない:
`Bookmark`は、データベース内部でそのレコードを一意に識別する「ID」のようなものです。これは単なる「○行目」という物理的な位置ではなく、データの論理的な実体を指し示します。そのため、フォームの`Requery`によってデータが再ソートされたり、レコードの追加・削除で物理的な行番号が変わったりしても、`Bookmark`は変わらずに元のレコードを指し続けることができるのです。

3. 使い分けの妙:

  • `Me.RecordsetClone`: 「フォームの表示データに基づき、表示位置を維持しながらレコードセット内を検索・操作したい」場合に最適です。
  • `CurrentDb.OpenRecordset`: 「フォームとは完全に独立して、別のテーブルやクエリからデータを取得し、操作したい」場合に用います。

どちらもレコードセットですが、その目的と役割を明確に区別することが、コードの可読性と堅牢性を高めます。

よくある落とし穴と注意点

  • Bookmarkが`Null`または無効な場合:

新規レコードや、レコードが一件も存在しないフォームでは、`Me.Recordset.Bookmark`は有効な値を返しません。コード例のように`IsNull`でチェックするか、`Me.Recordset.BOF`や`Me.Recordset.EOF`でレコードの存在を確認する習慣をつけましょう。

  • `Recordset`と`RecordsetClone`の混同:

`Me.Recordset`を直接操作すると、フォームの表示位置が動いてしまいます。表示位置を維持したい場合は、必ず`Me.RecordsetClone`を使って操作するか、`CurrentDb.Execute`などでバックエンドで処理するようにしましょう。

  • 大量データ更新時の考慮:

今回の例のように`CurrentDb.Execute`でSQLを実行するのは非常に効率的ですが、レコードを一件ずつ`RecordsetClone`で操作して更新する場合は、トランザクション処理(`DBEngine.BeginTrans`, `DBEngine.CommitTrans`, `DBEngine.Rollback`)を導入することで、処理中のエラーでデータが中途半端になるのを防ぎ、パフォーマンスも向上させることができます。

まとめ:あなたもAccess VBAマスターへの一歩を踏み出しましたね!

DAO.Recordsetの「Clone」と「Bookmark」は、Access VBAでフォームを自在に操るための、まさに「必殺技」のような存在です。
これらの概念を理解し、実際にコードに落とし込むことで、あなたはユーザーが「かゆいところに手が届く」ような、より洗練されたAccessアプリケーションを開発できるようになります。

フォームの表示位置がズレるという、一見小さな問題ですが、これを解決できるかどうかで、アプリケーションの使いやすさは大きく変わります。今日の学びが、あなたのAccess VBAスキルを次のレベルへと押し上げる一助となれば幸いです。

さあ、この知識を胸に、さらに奥深いAccess VBAの世界を探求していきましょう!
次回も、皆さんの開発をサポートする、とっておきのテクニックをお届けしますのでお楽しみに!

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