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

スポンサーリンク

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

開発リーダーの私だ。日々のAccessアプリ開発、ご苦労様。

数万件、数十万件に及ぶローカル/ネットワーク上のトランザクションデータを一括処理するバッチ処理を組んだとき、ユーザーから「画面が固まったんだけど、これバグってない?」と冷や汗モノの問い合わせを受けたことはないだろうか?

AccessのVBAで重いループ処理を回している最中、UIスレッドは完全にロックされる。ユーザーは何が起きているか分からず、苛立ってタスクマネージャーからAccessを強制終了させる……。これが現場で最も恐れられる「フリーズ誤認事故」だ。

この悪夢を防ぐためにステータスバーやプログレスバーを導入するわけだが、よくある素朴な実装はこうだ。

「とりあえず処理件数をカウントして、100件ごとに `DoEvents` を挟もう」

……甘い。それでは不十分だ。レコード件数が予測できないクエリ結果や、フィルタリングされたレコードセットを扱う際、「今全体全体の何パーセントが進んでいるのか」を正確かつノーコストで算出する方法を知らなければ、プログレスバーはただの「嘘つきのインジケーター」と化す。

今回は、DAOの `Recordset` が秘める強力なプロパティ `AbsolutePosition` にスポットを当て、実務の現場で耐えうる堅牢かつ滑らかな進捗表示の極意を伝授しよう。

なぜ「カウンター変数でのインクリメント」は実務で破綻するのか?

多くの初中級プログラマブルは、ループ内で以下のようなコードを書く。

‘ 【反面教師となるアンチパターン】
Dim rs As DAO.Recordset
Dim totalCount As Long, i As Long

Set rs = CurrentDb.OpenRecordset(“T_HeavyData”, dbOpenSnapshot)
rs.MoveLast: rs.MoveFirst ‘ 総件数を取得するための非効率なジャンプ
totalCount = rs.RecordCount

Do Until rs.EOF
‘ 重い処理
i = i + 1
If i Mod 100 = 0 Then
SysCmd acSysCmdSetStatus, i & ” / ” & totalCount & ” 処理中…”
DoEvents
End If
rs.MoveNext
Loop
rs.Close

このコードには、アーキテクトの視点から見ると3つの重大な罪がある。

1. `RecordCount` 取得のための無駄な往復コスト: `dbOpenSnapshot` や `dbOpenDynaset` での `MoveLast` は、全レコードをローカルキャッシュに引き込むため、巨大なテーブルでは致命的なオーバーヘッドになる。
2. カウンターの管理ミスのリスク: 条件分岐やエラーハンドリングで `i` のインクリメントがズレた瞬間、表示される進捗が実態と乖離する。
3. `DoEvents` の乱用によるパフォーマンス低下: 100件ごとの `DoEvents` はUIの応答性を保つが、毎回OSに制御を返すため、純粋な処理速度が低下する。

救世主 `AbsolutePosition` とは何か?

DAOの `Recordset` オブジェクトが持つ `AbsolutePosition` プロパティは、現在のレコードがレコードセット内の何番目に位置するか(0ベースのインデックス)を返す。

これの何が素晴らしいか?
わざわざ自前でカウンター変数をインクリメントしなくても、Recordset自身が「今の位置」を正確に把握している点だ。

さらに、Accessの `SysCmd` 関数と組み合わせることで、アプリケーションのステータスバーにスマートな進捗インジケーターをノーコストで描画できる。

【実践】プロダクションコード:堅牢な進捗制御クラス/プロシージャ

実務でそのままコピー&ペーストして組み込める、洗練されたモジュールコードを公開する。エラーハンドリング、トランザクションの安全な管理、そして「1%刻みでのみ描画を更新するスロットリング処理」を実装している。

Option Compare Database
Option Explicit

‘ ==============================================================================
‘ 概要: 大量データ処理時の進捗をSysCmdステータスバーに動的表示するプロシージャ
‘ 特徴: AbsolutePositionを活用し、カウンター変数なしで正確な進捗率を算出
‘ ==============================================================================
Public Sub ExecuteHeavyProcessWithProgress()
On Error GoTo ErrorHandler

Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim totalRecords As Long
Dim currentPos As Long
Dim currentPercent As Integer
Dim lastPercent As Integer

Set db = CurrentDb()

‘ 1. 対象レコードセットを開く(Snapshotを使用しメモリ負荷を最小化)
‘ ※ 実際の業務ではここに複雑なSQLやパラメータクエリが入る
Set rs = db.OpenRecordset(“SELECT FROM T_HeavyData WHERE Processed = 0”, dbOpenSnapshot)

‘ レコードが存在しない場合のガード節
If rs.EOF Then
MsgBox “処理対象のデータが存在しません。”, vbInformation, “通知”
GoTo Cleanup
End If

‘ 2. 正確な総件数の取得
‘ ※ DAOの仕様上、MoveLastを行わないと正確なRecordCountが取れない点に注意
rs.MoveLast
totalRecords = rs.RecordCount
rs.MoveFirst

‘ 3. ステータスバーの初期化(プロゲージ表示モード)
SysCmd acSysCmdInitMeter, “データ一括処理を実行中…”, totalRecords
lastPercent = -1 ‘ パーセンテージの重複更新を防ぐための制御変数

‘ 4. メインループ
Do Until rs.EOF

‘ ———————————————————————-
‘ ここに実際の重い業務ロジックを記述(例:別テーブルへの書き込みやAPI連携)
‘ ———————————————————————-
‘ Call ProcessEachRecord(rs!ID)
‘ ———————————————————————-

‘ AbsolutePositionから現在の位置(0始まり)を取得
currentPos = rs.AbsolutePosition

‘ パーセンテージを算出(ゼロ除算ガードは総件数チェック済みのため不要)
currentPercent = CInt((currentPos + 1) / totalRecords 100)

‘ 1%以上進捗が進んだ場合のみ、ステータスバーを更新(描画コストの極限抑制)
If currentPercent <> lastPercent Then
‘ ステータスバーのゲージを進める
SysCmd acSysCmdUpdateMeter, currentPos + 1
‘ テキストメッセージも同時に更新
SysCmd acSysCmdSetStatus, “処理中… ” & (currentPos + 1) & ” / ” & totalRecords & ” 件 (” & currentPercent & “%)”

‘ UIのフリーズを防ぎつつ、OSへの負荷を考慮してDoEventsを適切に配置
DoEvents

lastPercent = currentPercent
End If

rs.MoveNext
Loop

‘ 正常終了メッセージ
SysCmd acSysCmdRemoveMeter
SysCmd acSysCmdSetStatus, “処理が正常に完了しました。”
MsgBox totalRecords & “件の処理が完了しました。”, vbInformation, “完了”

Cleanup:
‘ 5. リソースの確実な解放(メモリリークの根絶)
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
Exit Sub

ErrorHandler:
‘ 異常発生時のクリーンアップとステータスバーの復元
SysCmd acSysCmdRemoveMeter
SysCmd acSysCmdSetStatus, “エラーが発生しましたため中断しました。”
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error: ” & Err.Description, vbCritical, “システムエラー”
Resume Cleanup
End Sub

アーキテクトが解説する、このコードのキモ

1. `AbsolutePosition` と `+1` の魔術

`AbsolutePosition` は `0` からスタートする。つまり、1件目のレコードを処理している最中の値は `0` だ。
そのため、`currentPos + 1` とすることで、「現在の処理件数(1-origin)」を正確に弾き出すことができる。自前のインクリメント変数(`i = i + 1`)を排除できるため、コードの可読性が上がり、バグの温床を完全に断つことができる。

2. 描画スロットリング(Throttling)によるパフォーマンス最適化

ループの1回ごとに `SysCmd acSysCmdUpdateMeter` や `DoEvents` を呼ぶと、それ自体がオーバーヘッドになり、処理スピードが体感で数倍遅くなる。
コード内の `If currentPercent <> lastPercent Then` という条件分岐に注目してほしい。これにより、「パーセンテージが変動した瞬間(つまり最大でも100回)」しかUI描画を行わない
数万件のループであっても、描画処理のコストを極限まで削ぎ落とし、爆速かつ滑らかなプログレスバーを実現している。

3. `SysCmd` によるスマートなUI制御

ユーザーフォーム(UserForm)をわざわざ作ってプログレスバーを配置する開発者もいるが、Access標準のステータスバー(`SysCmd acSysCmdInitMeter`)を使うべきだ。
余計なフォームのロード時間を排除でき、どの画面からでも一貫したUXを提供できる。さらに、エラー時や終了時の `SysCmd acSysCmdRemoveMeter` による後始末も完璧に組み込んである。

現場でありがちなトラブルと対策(FAQ)

  • Q: `AbsolutePosition` が `-1` を返すことがあるんだけど?
  • A: レコードセットのタイプが `dbOpenForwardOnly`(前進専用)の場合、`AbsolutePosition` はサポートされず `-1` を返す。必ず `dbOpenSnapshot` または `dbOpenDynaset` を指定すること。
  • Q: ネットワーク越しのバックエンドDB(SQL Server等)だと、`MoveLast` が重いのでは?
  • A: その通り。ODBC経由のリンクテーブルに対して `MoveLast` を行うと、全レコードのフェッチが発生してネットワーク帯域を圧迫する。もしデータ量が莫大な場合は、あらかじめ `SELECT COUNT()` で別クエリから総件数を取得する設計(パスカウンター方式)に切り替えるべきだ。

総括

プログラミングの質は、「動くものを作る」段階から、「保守しやすく、ユーザーのストレスをゼロにするものを作る」段階へシフトしたときに一段階跳ね上がる。

今回紹介した `AbsolutePosition` を使った進捗制御は、単なる見た目の演出ではない。「システムが生きていること(ハングアップしていないこと)」をユーザーに確信させ、無用な強制終了や問い合わせを防ぐ、極めてロジカルなリスクヘッジである。

君の作るAccessアプリが、プロフェッショナルな洗練されたツールへと生まれ変わることを期待している。

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