Access VBAの現実:なぜ「終わらない処理」は現場の信頼を失うのか
業務システムの現場において、ユーザーが最もストレスを感じる瞬間は何か。それは、数万件規模のデータ更新や集計バッチを実行した際、画面が完全にフリーズし、「本当に動いているのか、それともAccessがクラッシュしたのか」すら分からない沈黙の時間が訪れる瞬間だ。
「しばらくお待ちください」という無機質なメッセージボックスを表示して画面を閉じ、タスクマネージャーを恐る恐る開く――。この悪しきユーザー体験(UX)を放置している開発者は、プロとして失格と言わざるを得ない。
長時間のデータ処理において、進捗バー(プログレスバー)を正しく実装することは、単なる「見た目の演出」ではない。システムの透明性を担保し、ユーザーの無用な強制終了を防ぐための生命線なのだ。
そして、この進捗管理を最も美しく、かつ最も軽量に実現できるのが、DAO(Data Access Objects)の `Recordset` が持つ `AbsolutePosition` プロパティ である。
今回は、数百万件をも飲み込むAccessの裏側で、DAOとフォームを完璧に調停し、実用に耐えうる堅牢な進捗管理システムを構築する極限の知見を伝授する。
—
愚劣な実装のアンチパターン:なぜ `DCount` や不完全なループは爆発するのか
多くの初中級プログラマが陥る罠が、進捗率を計算するために無駄なクエリを走らせる実装だ。
‘ 【やってはいけない最悪の例】
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim totalCount As Long
Dim currentCount As Long
Set db = CurrentDb
Set rs = db.OpenRecordsets(“T_HeavyData”, dbOpenDynaset)
‘ 愚かにも全件数を取得するために毎回 DCount を叩く、あるいは RecordCount のために MoveLast を呼ぶ
totalCount = DCount(“”, “T_HeavyData”)
Do While Not rs.EOF
‘ 重い処理
currentCount = currentCount + 1
‘ 毎回ステータスバーやプログレスバーを更新(画面描画のオーバーヘッドで絶望的に遅くなる)
SysCmd acSysCmdSetStatus, “処理中: ” & Format(currentCount / totalCount, “0%”)
rs.MoveNext
Loop
このコードの何が問題か。
1. 二重の負荷: レコードセットを回している最中に、別の集計処理や不適切な `RecordCount` の取得を行うことで、Jet/ACEエンジンに無駄なI/Oが発生する。
2. 描画の呪い: ループの1回ごとにステータスバーやフォームを更新すると、VBAのインタープリターとWindowsの描画スレッドが飽和し、処理速度が通常の10分の1以下に落ち込む。
3. カプセル化の崩壊: 途中でエラーが発生した際の状態復旧や、ユーザーによるキャンセル処理(`DoEvents`の適切な制御)が抜けている。
プロフェッショナルは、「DAOのポインタ位置そのものを進捗の指標に変換する」。ここに `AbsolutePosition` の真価がある。
—
`AbsolutePosition` の仕様と、プロフェッショナルが知るべき暗黙のルール
`DAO.Recordset.AbsolutePosition` は、現在のレコードセット内での相対的な位置を 0 から始まるインデックスで返すプロパティだ。つまり、今自分が何番目のレコードを処理しているのかを、追加のカウント変数なしで一撃で取得できる。
しかし、このプロパティには致命的な「落とし穴」と「前提条件」がある。
1. `RecordCount` の完全なポップアップが必須:
`AbsolutePosition` を正確に機能させるためには、DAOの内部バッファに対して「このレコードセットには全体で何件あるのか」を確定させなければならない。そのためには、レコードセットを開いた直後に `rs.MoveLast` を実行し、さらに `rs.MoveFirst` でポインタを先頭に戻すという儀式が絶対不可欠である。
(※これを行わないと、`AbsolutePosition` は常に `-1` を返すか、正確な位置を返さない)
2. パフォーマンスのトレードオフ:
`MoveLast` は全レコードをローカルキャッシュに読み込むため、巨大なテーブル(数百万件)ではわずかに入口でウェイトが入る。しかし、全体の処理時間を考えれば、正確な進捗の分母(`RecordCount`)を得るための必要経費である。
—
【実装コード】コピペで使える堅牢なプロダクションコード
ここからは、実際の業務アプリ(保守性・パフォーマンス・UXを極限まで高めた設計)でそのまま使えるモジュールを公開する。
進捗表示には、Access標準のステータスバー(`SysCmd`)と、フォーム上のプログレスバー(矩形ラベル等で代用可能、あるいは簡易的なステータス表示)の両方に対応できる構造とする。
1. 進捗管理を内包したメイン処理プロシージャ
Option Compare Database
Option Explicit
‘ =========================================================================
‘ 処理名 :
‘ 概要 : 大容量レコードセットを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 progressRate As Double
Dim startTime As Double
startTime = Timer
Set db = CurrentDb
‘ 1. ダイナセットまたはスナップショットでレコードセットを開く
‘ ※ 大量データ処理では読取専用の dbOpenSnapshot を推奨(ロック競合を防ぐ)
Set rs = db.OpenRecordsets(“SELECT FROM T_TargetData WHERE ProcessedFlag = False”, dbOpenSnapshot)
‘ 2. レコードが存在しない場合のガード節
If rs.EOF Then
MsgBox “処理対象のデータが存在しません。”, vbInformation, “情報”
GoTo Cleanup
End If
‘ 3. 【最重要】AbsolutePositionを有効化するために全体件数を確定させる
rs.MoveLast
totalRecords = rs.RecordCount
rs.MoveFirst
‘ 4. ステータスバーの初期化 (SysCmd API)
SysCmd acSysCmdInitMeter, “データ処理を実行中…”, totalRecords
‘ 5. メインループ
Do While Not rs.EOF
‘ — [業務ロジックの実行エリア] —
‘ 例として、何らかのデータ加工や別テーブルへの書き込みを想定
Call ProcessSingleRecord(rs)
————————————
‘ AbsolutePositionの取得 (0始まりなので +1 する)
currentPos = rs.AbsolutePosition + 1
‘ 一定間隔(例: 100件ごと、または最終行)でのみUIを更新し、描画負荷を極限まで軽減する
If currentPos Mod 100 = 0 Or currentPos = totalRecords Then
‘ ステータスバーの更新
SysCmd acSysCmdUpdateMeter, currentPos
‘ UIのフリーズを防ぎつつ、ユーザーの操作(キャンセル等)を受け付ける
DoEvents
End If
rs.MoveNext
Loop
‘ 完了時の処理
SysCmd acSysCmdRemoveMeter
MsgBox “処理が正常に完了しました。” & vbCrLf & _
“処理件数: ” & Format(totalRecords, “#,
0″) & ” 件” & vbCrLf & _
“所要時間: ” & Format(Timer – startTime, “0.0”) & ” 秒”, vbInformation, “完了”
Cleanup:
‘ リソースの解放(メモリリークの完全防止)
On Error Resume Next
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
‘ ダミーの個別処理プロシージャ
Private Sub ProcessSingleRecord(ByRef rs As DAO.Recordset)
‘ ここに実際のフィールド値の読み取りや更新処理を記述する
‘ 例: Debug.Print rs!ID
End Sub
—
アーキテクトが教える、現場で絶対に押さえるべき「3つの鉄則」
上記のコードをプロジェクトに組み込むにあたり、チーフアーキテクトとして妥協してはならない設計上のポイントを解説する。
1. `dbOpenSnapshot` の積極的活用
もしデータを「更新」するのではなく、「集計・移行・出力」するだけのバッチであれば、レコードセットのタイプは必ず `dbOpenSnapshot` を指定しろ。ダイナセット(`dbOpenDynaset`)は変更を監視するためのオーバーヘッドが伴うため、数万件以上のループではパフォーマンスに致命的な差が出る。
2. UI更新の「間引き(Throttle)」
ループの1回ごとに `SysCmd` や `DoEvents` を実行してはならない。Windowsのメッセージキューがパンクし、かえって処理が遅延する。コード例のように `currentPos Mod 100 = 0`(100件に1回) のようにスロットリング(間引き)を入れることで、滑らかなバーの動きと爆速の処理スピードを両立できる。
3. 例外処理における「ステータスバーの死活管理」
`SysCmd acSysCmdInitMeter` で初期化したステータスバーは、エラーが発生して途中でプロシージャが抜けた場合、Accessの画面下部にバーが残ったままフリーズしたような状態になる。これを防ぐため、必ず `ErrorHandler` ラベル内や `Cleanup` セクションで `SysCmd acSysCmdRemoveMeter` を呼び出す構造(Try-FinallyイディオムのVBA版)を徹底すること。
—
結び:コードの美しさは、ユーザーの信頼に直結する
Accessは、正しく設計されれば、基幹系に匹敵する堅牢な局所ソリューションとして今なお最強のプラットフォームであり続ける。
「動けばいい」というスパゲッティコードから脱却し、正確なプログレス管理とリソースのライフサイクル管理が組み込まれたコードを書くこと。それこそが、現場のユーザーから「このシステムは信頼できる」と言わしめる唯一の道である。
明日からの開発で、安易な `DCount` や無言のフリーズ画面をコードから駆逐せよ。あなたの書くVBAの品質が、組織の生産性を何段階も引き上げるのだから。
