【テクニカル・上級編】【中級】DAO.Recordsetの「AbsolutePosition」を活用した、進捗バーの動的更新とユーザー体験の向上 – Access VBA解析バイブル

スポンサーリンク

【中級〜上級】DAO.Recordsetの「AbsolutePosition」を活用した、進捗バーの動的更新とユーザー体験の向上

レガシーシステムと呼ばれる領域であっても、現場のユーザビリティに対する要求は年々シビアになっている。数万件、あるいは数十万件のレコードを伴うバッチ処理を実行した瞬間、画面が白濁し、「フリーズしたのか?」と不安に駆られたユーザーがタスクマネージャーからAccessを強制終了させる——。これは、日本の多くの現場でいまだに繰り返されている悲劇だ。

「処理が終わるまで待て」というエンジニア側の傲慢は、システムの信頼性を静かに、しかし確実に蝕む。

今回は、DAO(Data Access Objects)の `Recordset` が持つ `AbsolutePosition` プロパティを極限まで引き出し、重厚なループ処理に命を吹き込む進捗バー(ステータスバー)の実装テクニックを解説する。教科書的な「とりあえずDoEventsを挟む」という次元を超え、メモリ効率とUIの応答性を両立させるプロフェッショナルの実装を紐解く。

1. なぜ `Recordset.AbsolutePosition` なのか?

大量データの処理において、進捗率(パーセンテージ)を算出するための最大の壁は「全体のレコード数を事前に正確に把握するコスト」「ループごとの演算コスト」のトレードオフだ。

よく見かけるアンチパターンとして、`DCount` 関数をループの事前や最中に実行する愚行がある。あれはインデックスが効いていない限り、テーブルの全スキャンを発生させ、データベースエンジンに致命的な負荷を与える。

ここで `DAO.Recordset`(特に `dbOpenDynaset` や `dbOpenSnapshot`)の `AbsolutePosition` プロパティの出番となる。

AbsolutePositionの本質と罠

`AbsolutePosition` は、レコードセット内における現在のレコードの「0基底のインデックス(何番目のレコードか)」を返す。
しかし、ここにはシニアエンジニアなら知っておくべき重大な前提がある。

> 「RecordCountが完全にポピュレート(確定)されていなければ、正確な位置を返さない」

順方向のみの `dbOpenForwardOnly` などの軽量なカーソルでは、最後まで読み切るまで全体数が確定しないため、途中で `AbsolutePosition` を参照しても `-1` を返すか、不正確な値になる。したがって、進捗率を算出するためには、適切なカーソルタイプの選択と、事前のレコード数確定の儀式が必要不可欠なのだ。

2. 実装アーキテクチャ:動的進捗表示のフルコード

以下のコードは、単に動くだけの代物ではない。メモリリークの防止、画面描画の負荷軽減(インターバル制御)、そしてエラーハンドリングの堅牢性を兼ね備えた実戦投入可能なモジュールである。

Option Compare Database
Option Explicit

‘ =========================================================================
‘ 処理名: ProcessLargeDataWithProgress
‘ 概要 : DAO.RecordsetとAbsolutePositionを用いた、最適化された進捗バー更新処理
‘ =========================================================================
Public Sub ProcessLargeDataWithProgress()
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim totalRecords As Long
Dim currentPos As Long
Dim progressPct As Integer
Dim lastUpdatedPct As Integer

‘ パフォーマンス計測用(必要に応じて)
Dim startTime As Double
startTime = Timer

On Error GoTo ErrorHandler

Set db = CurrentDb

‘ 【重要】AbsolutePositionを正確に機能させるため、
‘ SnapshotまたはDynasetを使用し、かつRecordCountを強制評価させる。
‘ dbOpenSnapshotは読み取り専用かつ最速のパフォーマンスを提供する。
Set rs = db.OpenRecordset(“SELECT FROM T_TargetData WHERE Processed = 0”, dbOpenSnapshot)

‘ レコードが存在しない場合のガード
If rs.RecordCount = 0 Then
MsgBox “処理対象のレコードが存在しません。”, vbInformation, “情報”
GoTo Cleanup
End If

‘ RecordCountの完全評価(DAOの仕様上、MoveLastを叩くことで確実に総数を確定させる)
rs.MoveLast
totalRecords = rs.RecordCount
rs.MoveFirst

‘ ステータスバーの初期化
SysCmd acSysCmdInitMeter, “データを処理しています…”, totalRecords
lastUpdatedPct = -1

‘ メインループ
Do Until rs.EOF
‘ —————————————————————–
‘ ここに実際の業務ロジック(データ更新、外部API連携など)を記述
‘ —————————————————————–
‘ 例: Debug.Print rs!ID
‘ —————————————————————–

‘ 現在位置の取得(0基底なので +1 する)
currentPos = rs.AbsolutePosition + 1

‘ パーセンテージの算出
progressPct = CInt((currentPos / totalRecords) 100)

‘ 【最適化】UIの更新頻度を制御する
‘ すべてのレコードでSysCmdやDoEventsを呼ぶと、それだけでI/Oとコンテキストスイッチの
‘ 膨大なオーバヘッドが発生し、処理速度が数倍〜数十倍に落ちる。
‘ 「1パーセント進んだ時のみ」UIを更新するようスロットリング(間引き)を行う。
If progressPct <> lastUpdatedPct Then
SysCmd acSysCmdUpdateMeter, currentPos

‘ UIスレッドに処理を返し、ユーザーの「フリーズ誤認」を防ぐとともに、
‘ 「キャンセル」等の割り込み操作を受け付け可能にする
DoEvents

lastUpdatedPct = progressPct
End If

rs.MoveNext
Loop

‘ ステータスバーのクリア
SysCmd acSysCmdRemoveMeter

MsgBox “処理が正常に完了しました。” & vbCrLf & _
“処理件数: ” & totalRecords & “件” & vbCrLf & _
“処理時間: ” & Format(Timer – startTime, “0.00”) & “秒”, vbInformation, “完了”

Cleanup:
‘ 【鉄則】オブジェクトの明示的解放
‘ アクセスのガベージコレクションに依存せず、スコープを抜ける前に必ず破棄する。
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
Exit Sub

ErrorHandler:
‘ 予期せぬエラー発生時にもステータスバーの残骸を残さない
SysCmd acSysCmdRemoveMeter
MsgBox “エラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical, “システムエラー”
Resume Cleanup
End Sub

3. チーフアーキテクトが教える「現場の知見」と最適化の極意

上記のコードには、何十年ものレガシーシステム保守から導き出された「痛みの知恵」が詰まっている。単なるコードの模倣ではなく、以下のアーキテクチャ上の設計思想を理解してほしい。

① `DoEvents` の悪夢と「スロットリング(間引き)」

`DoEvents` は、VBAのシングルスレッドモデルにおいてUIの応答性を保つ唯一の救済措置だが、「多用すると処理速度が劇的に低下する」という諸刃の剣だ。
10万件のループで毎回 `DoEvents` とステータスバーの再描画を走らせると、Windowsのメッセージキューが飽和し、純粋な演算処理時間の10倍以上のオーバーヘッドを生む。
前述のコードのように `If progressPct <> lastUpdatedPct Then` で条件分岐させ、「値が変化した1%の瞬間のみUIを叩く」設計にすることで、描画コストを最大1/100に削減しつつ、滑らかなプログレスバーを実現できる。

② DAOレコードセットのライフサイクル管理

Access VBA開発者で最も多いバグが、オブジェクトの解放漏れによるメモリリークや、ファイルロックの残留だ。
特に `CurrentDb` は呼び出すたびに新しいデータベースインスタンスのクローンを生成するため、ループ内で安易に `CurrentDb.OpenRecordset` を叩くのは自殺行為である。
必ずローカル変数 `db` と `rs` に格納し、`Cleanup` ラベルへジャンプするクリーンアップパターンを徹底すること。例外発生時(`Error GoTo`)であってもステータスバーの消去(`SysCmd acSysCmdRemoveMeter`)とオブジェクトの破棄が確実に行われる構造にしなければ、Accessのステータスバーが永遠に進捗表示モードのまま固定され、最悪の場合アプリケーションの再起動を強いられる。

③ ネットワーク環境(Client/Server構成)における注意点

もしこのデータベースがバックエンド(SQL Serverなど)をODBCリンクテーブルとして接続している、あるいはACCDE/ACCD_でマルチユーザー環境下にある場合、`dbOpenSnapshot` の選択が極めて重要になる。
`dbOpenDynaset` は双方向の更新が可能だが、ロックやネットワークトラフィックのオーバーヘッドが大きい。読み取り専用のバッチ処理や進捗監視であれば、必ず `dbOpenSnapshot` を用いてローカルキャッシュに近い状態でメモリ上に展開すべきである。

結びにかえて

「動けばいい」というコードは、プロトタイプとしては価値があっても、プロダクション環境では爆弾でしかない。

今回紹介した `AbsolutePosition` を活用した進捗管理は、単に見栄えを良くするためだけの装飾ではない。「システムが生きていること(稼働していること)をユーザーに証明し、心理的な不安を取り除く」という、エンタープライズシステムにおいて最も重要な信頼性の担保なのだ。

あなたの書くVBAコードが、単なる「スクリプト」から、堅牢な「アーキテクチャ」へと昇華することを期待している。

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