【実務・中級編】DAOの「dbOpenDynaset」と「dbOpenSnapshot」の使い分け:フォームの読み込み速度を劇的に改善する – Access VBA解析バイブル

スポンサーリンク
スポンサーリンク

「とりあえずダイナセット」がシステムを殺す。DAOカーソルタイプを極限まで使いこなす設計思想

業務自動化の現場で、中途半端なエンジニアが真っ先に陥る罠がある。それは、「データの読み込み速度が遅い」「フォームの切り替えが重い」という問題に対し、インデックスの追加やSQLの書き換えといった、表面的な対策に終始することだ。

真のアーキテクトは、データへの「窓口」であるカーソルタイプ(Cursor Type)の選択にこそ、システムの生命線を託す。

今回は、DAO(Data Access Objects)における2つの主要なカーソルタイプ、`dbOpenDynaset` と `dbOpenSnapshot` の決定的な違いを解剖する。これを理解せずして、プロフェッショナルなAccessアプリケーションの構築は不可能だ。

1. 概念の解剖:Snapshot と Dynaset の本質的な違い

まず、君たちが書いているコードの背後で、何が起きているのかを正確に理解せよ。

dbOpenSnapshot:静止画としてのデータ

`dbOpenSnapshot` は、その名の通り、開いた瞬間のデータの「スナップショット(静止画)」をメモリ上に構築する。

  • 特性: 読み取り専用(Read-Only)。開いた後に元のテーブルが更新されても、このレコードセットには反映されない。
  • 利点: 圧倒的な高速レスポンス。データ構造が固定されているため、エンジンは最小限のオーバーヘッドでデータを展開できる。
  • 欠点: データの更新ができない。最新の状態を常に追うことはできない。

dbOpenDynaset:動的なストリームとしてのデータ

`dbOpenDynaset` は、データベースの「現在の状態」を常に反映しようとする、動的なカーソルだ。

  • 特性: 読み書き可能(Read-Write)。レコードセットを開いている間に他のユーザーがデータを更新すれば、その変更が反映される。
  • 利点: データの編集・更新が自在。リアルタイム性が高い。
  • 欠点: 動作が重い。データの整合性を保つためのロック管理や、変更を検知するためのメカニズムがバックグラウンドで常に走り続けている。

2. なぜ「とりあえずDynaset」ではいけないのか?

多くの開発者が、コードの書きやすさから、あるいは「後で編集するかもしれない」という過剰な設計思想から、すべてを `dbOpenDynaset` で開こうとする。これがパフォーマンス低下の元凶だ。

リソースの浪費

`dbOpenDynaset` は、レコードの変更を追跡するために、メモリとディスクI/Oを大量に消費する。数千件、数万件のレコードを扱うフォームでこれを多用すれば、ユーザーは「砂時計」を見続けることになるだろう。

ロック競合の増大

Dynasetは、データの整合性を守るために、データベースに対してより強力なロックを要求する。これにより、マルチユーザー環境において「他のユーザーが使用中のため、操作できません」というエラーが頻発する。設計ミスが、ユーザー体験を直接破壊しているのだ。

3. プロフェッショナルな使い分けの判断基準

設計時に、以下のマトリックスを脳内に叩き込め。

用途 推奨カーソルタイプ 理由
一覧表示用フォーム (検索結果、ログ、履歴) `dbOpenSnapshot` 編集の必要がなく、表示速度が最優先されるため。
マスタ参照用コンボボックス `dbOpenSnapshot` 読み込み速度がUIのレスポンスに直結するため。
データ入力・編集用フォーム `dbOpenDynaset` ユーザーの変更を即座にDBへ書き戻す必要があるため。
集計・レポート用処理 `dbOpenSnapshot` 計算の途中でデータが変わると、整合性が崩れるリスクがあるため。

4. 実践:保守性と堅牢性を両立したプロダクションコード

では、実際の現場で使えるコードを示そう。単にRecordsetを開くだけではない。「エラーハンドリング」「オブジェクトの確実な解放」「用途に応じたモード切り替え」を組み込んだ、プロフェッショナルな実装だ。

以下のコードは、データアクセス層(DAL)として機能するように設計されている。

Option Compare Database
Option Explicit

' ======================================================================
' Module: modDataAccess
' Description: データアクセスを制御する低レイヤーモジュール
' ======================================================================

Public Enum CursorMode
    cmSnapshot    ' 高速・読み取り専用
    cmDynaset     ' 更新可能・動的
End Enum

''' <summary>
''' 指定されたSQLに基づき、最適なカーソルタイプでRecordsetを返却する。
''' </summary>
''' <param name="sql">実行するSQL文</param>
''' <param name="mode">CursorMode (cmSnapshot or cmDynaset)</param>
''' <returns>DAO.Recordset オブジェクト</returns>
Public Function GetRecordset(ByVal sql As String, ByVal mode As CursorMode) As DAO.Recordset
    On Error GoTo Err_Handler

    Dim db As DAO.Database
    Dim rs As DAO.Recordset
    Dim cursorType As Long

    Set db = CurrentDb

    ' カーソルタイプの決定
    Select Case mode
        Case cmSnapshot
            cursorType = dbOpenSnapshot
        Case cmDynaset
            cursorType = dbOpenDynaset
        Case Else
            Err.Raise vbObjectError + 513, "GetRecordset", "無効なカーソルモードが指定されました。"
    End Select

    ' Recordsetの生成
    ' オプションとして dbCompareDatabase を指定することで、
    ' リンクテーブル等の比較精度を安定させる。
    Set rs = db.OpenRecordset(sql, cursorType, dbCompareDatabase)

    ' 戻り値の設定
    Set GetRecordset = rs

Exit_Handler:
    ' 注意: ここで db や rs を Close/Set Nothing してはいけない。
    ' 呼び出し元が rs を使用するため、ライフサイクルは呼び出し側に委ねる。
    Exit Function

Err_Handler:
    MsgBox "データ取得中に致命的なエラーが発生しました。" & vbCrLf & _
           "Error No: " & Err.Number & vbCrLf & _
           "Description: " & Err.Description, vbCritical, "Data Access Error"
    Set GetRecordset = Nothing
    Resume Exit_Handler
End Function

''' <summary>
''' フォームのレコードソースを動的に切り替える例(フォームモジュールに記述)
''' </summary>
Public Sub RefreshFormSource(frm As Form, isEditMode As Boolean)
    On Error GoTo Err_Handler

    Dim rs As DAO.Recordset
    Dim strSql As String
    Dim mode As CursorMode

    ' 1. 用途に応じたSQLとモードの切り替え
    If isEditMode Then
        strSql = "SELECT * FROM T_MasterTable WHERE Status = 'Active';"
        mode = cmDynaset
    Else
        strSql = "SELECT * FROM T_MasterTable WHERE Status = 'Active';"
        mode = cmSnapshot
    End If

    ' 2. 前のRecordsetを確実に解放(メモリリーク防止)
    If Not frm.Recordset Is Nothing Then
        frm.Recordset.Close
        Set frm.Recordset = Nothing
    End If

    ' 3. 高性能なRecordsetの取得
    Set rs = GetRecordset(strSql, mode)

    ' 4. フォームへのバインド
    If Not rs Is Nothing Then
        Set frm.Recordset = rs
    Else
        ' データが空の場合のハンドリング
        frm.Recordset.SetEOF
    End If

Exit_Handler:
    Exit Sub

Err_Handler:
    MsgBox "フォームの更新に失敗しました: " & Err.Description, vbExclamation
    Resume Exit_Handler
End Sub

このコードの設計思想(アーキテクトの視点)

1. Enumによる抽象化: `dbOpenSnapshot` という定数を直接使うのではなく、`CursorMode` という独自の列挙型を定義している。これにより、開発者は「何のためにそのモードを選ぶのか」という意図をコードに込めることができる。
2. ライフサイクル管理: `GetRecordset` 内で `Set db = CurrentDb` を行っているが、`db` オブジェクトの解放は関数の終了と共に自動的に行われる(ローカル変数であるため)。一方、`rs` は呼び出し元に渡すため、関数内で `Set Nothing` してはいけない。この「所有権の移譲」を理解せよ。
3. 堅牢なエラーハンドリング: データベース接続エラーは、必ず発生する。それを無視して進めることは、地雷原を全力疾走することと同じだ。
4. メモリリークへの配慮: フォームの `Recordset` を切り替える際、既存の `Recordset` を明示的に `Close` している。これを怠ると、Accessのメモリ使用量は雪だるま式に増え、最終的にクラッシュする。

結論:設計に「意図」を持て

「動くコード」を書くのは誰にでもできる。しかし、「最適に動くコード」を書けるのは、オブジェクトの振る舞いとリソースの重みを知る者だけだ。

フォームを開くとき、君はそのレコードセットが「見るためのもの」なのか、「書き換えるためのもの」なのかを自問自答せよ。もし、単なる表示用であるならば、迷わず `dbOpenSnapshot` を選択せよ。その一歩が、アプリケーションのプロフェッショナリズムを決定づける。

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