Accessの「画面フリーズ」を終わらせる:非同期クエリ実行の極限設計術
Accessで数百万件のレコードを扱うクエリを実行した瞬間、画面が白くなり「応答なし」と表示される。この「UIの死」は、業務アプリ開発者にとって最大の屈辱です。
多くの開発者は、DAOの`Execute`や`OpenRecordset`を同期的に実行し、処理が終わるまでユーザーを放置します。しかし、真のプロフェッショナルは、「重い処理はバックグラウンドに追いやり、ユーザーに操作の自由を与える」という設計思想を貫きます。
今回は、DAOの`QueryDef`を「疑似非同期」で実行し、UIを凍結させずに完了をフリーズさせるための、プロダクションレベルのアーキテクチャを伝授します。
—
なぜ「同期実行」は罪なのか
AccessのUIスレッドはシングルスレッドです。重いクエリが走っている間、VBAは次の行へ進めず、Windowsのメッセージループも停止します。結果、OSは「このアプリは死んでいる」と判断し、無慈悲に「応答なし」を突きつけます。
これを回避する唯一の正攻法は、「処理を分割し、タイマーイベントで細切れに監視する」こと。これが今回紹介する設計の核心です。
—
設計コンセプト:ステートマシンによる非同期制御
本手法では、以下の3つの要素を組み合わせます。
1. DAO.QueryDef: パラメータ化されたSQLを事前にコンパイルし、実行速度を最大化する。
2. DAO.Recordset (非同期接続): `dbOpenForwardOnly`と`dbSeeChanges`を駆使し、オーバーヘッドを最小化する。
3. Form.TimerInterval: UIスレッドを占有せず、定期的に完了チェックを行う監視機構。
—
実装:AsyncQueryExecutorクラスの設計
実務で使い回すため、ロジックをフォームから切り離し、クラスモジュール化します。
1. クラスモジュール:`clsAsyncQuery`
‘ クラス名: clsAsyncQuery
Option Compare Database
Option Explicit
Private m_qdf As DAO.QueryDef
Private m_rs As DAO.Recordset
Private m_isExecuting As Boolean
Public Event OnComplete(rs As DAO.Recordset)
Public Event OnError(ErrDescription As String)
‘ クエリの実行開始
Public Sub ExecuteAsync(queryName As String, ParamArray params() As Variant)
On Error GoTo ErrHandler
Set m_qdf = CurrentDb.QueryDefs(queryName)
‘ パラメータの動的バインド
Dim i As Integer
For i = LBound(params) To UBound(params)
m_qdf.Parameters(i).Value = params(i)
Next i
‘ 非同期実行の準備(ここではカーソルを開く準備のみを行う)
m_isExecuting = True
Set m_rs = m_qdf.OpenRecordset(dbOpenForwardOnly, dbSeeChanges)
m_isExecuting = False
RaiseEvent OnComplete(m_rs)
Exit Sub
ErrHandler:
m_isExecuting = False
RaiseEvent OnError(Err.Description)
End Sub
2. フォーム側での実装(タイマーの活用)
フォームの`Timer`イベントで、ステータスを監視します。これにより、処理中もプログレスバーを動かしたり、キャンセルボタンを有効にすることが可能です。
‘ フォームモジュール
Private WithEvents asyncExecutor As clsAsyncQuery
Private Sub Form_Load()
Set asyncExecutor = New clsAsyncQuery
End Sub
‘ 実行ボタン押下
Private Sub btnExecute_Click()
Me.TimerInterval = 100 ‘ 監視開始
asyncExecutor.ExecuteAsync “qry_HeavyTask”, Me.txtParam1.Value
End Sub
‘ 完了通知
Private Sub asyncExecutor_OnComplete(rs As DAO.Recordset)
Me.TimerInterval = 0
MsgBox “処理が完了しました!”, vbInformation
‘ ここでrsを処理
End Sub
‘ エラー通知
Private Sub asyncExecutor_OnError(ErrDescription As String)
Me.TimerInterval = 0
MsgBox “エラー発生: ” & ErrDescription, vbCritical
End Sub
—
現場で勝つための「極限の知見」
1. `dbOpenForwardOnly` の重要性
`Recordset`を開く際、デフォルトの`dbOpenDynaset`は強力ですが、全レコードのメタデータをキャッシュしようとします。数百万件のデータに対してこれを行うとメモリが枯渇します。`dbOpenForwardOnly`は、ストリームのようにデータを読み込むため、極めて軽量かつ高速です。
2. トランザクションとファイルロック
非同期処理中に別のユーザーが同じテーブルを更新すると、Accessの排他制御が競合します。`dbSeeChanges`オプションは必須です。これがないと、SQL Server連携時に「他のユーザーが変更しました」というエラーで処理が止まります。
3. タイマーイベントの罠
`TimerInterval`を短くしすぎると、UIスレッドが監視処理で埋め尽くされ、かえって重くなります。実務では`500ms~1000ms`程度が適正です。ユーザー体験を向上させるには、進捗を表すアニメーション(GIFや動的なラベル表示)をこのタイマーの中で制御してください。
—
結論
Accessは、設計次第で「限界」を突破できます。
今回のコードは、単に「止まらない」だけでなく、「エラーハンドリングをカプセル化し、UIとロジックを分離する」という、保守性を重視した設計です。
「画面が固まるのは仕方ない」と諦めるのは今日まで。
あなたの作るツールが、プロフェッショナルの道具として、軽快かつ堅牢に稼働することを期待しています。
