Access VBAを掌握する極限の知見:ADO非同期処理による「UIフリーズ」の完全破壊
Access VBAで大規模なデータベース(SQL ServerやAzure SQL、あるいはギガバイト級のバックエンドMDB/ACCDB)を運用する際、回避不能な壁として立ちはだかるのが「長時間のクエリ実行に伴うUIのフリーズ(応答なし)」である。
一般的なエンジニアはこれを「VBAの仕様(シングルスレッド限界)」として諦め、ユーザーに無機質な砂時計カーソルを強制する。しかし、COMアーキテクチャとADO(ActiveX Data Objects)の深層に踏み込めば、AccessのSTA(Single-Threaded Apartment)モデルを維持したまま、バックグラウンドで重厚なクエリを処理し、フロントエンドのUI応答性を100%維持することが可能となる。
本稿では、`adAsyncExecute`フラグを利用した非同期クエリの制御、Windows APIを用いた制御ループの最適化、そしてイベント駆動型(`WithEvents`)による状態監視の実装パターンを極限まで深掘り解説する。
—
1. なぜAccessのUIはフリーズするのか:COM STAと同期I/Oの真実
DAO(Data Access Objects)や通常のADO同期呼び出しでは、クエリ実行要求を発行した瞬間から、データベースエンジン(OLE DBプロバイダ / ODBCドライバー)が結果を返すまで、VBAのメインスレッド(STA)が完全なブロック状態(Wait State)に入る。
[同期実行のタイムライン]
メインスレッド (UI): –[クエリ発行]========(フリーズ状態)========>[結果受信]–
この間、Windowsのメッセージキューは処理されず、Accessのウィンドウ描画、ボタンのクリックイベント、OSからの再描画要求(`WM_PAINT`)はすべて不達となる。これが「応答なし」の正体である。
対して、ADOの非同期実行オプション(`adAsyncExecute`)を使用すると、ADOは内部的にWorker Thread(OSレベルのスレッド)へI/O処理を委譲し、VBAメインスレッドへ即座に制御を返却する。
[非同期実行のタイムライン]
メインスレッド (UI): –[クエリ発行]->[UI描画/APIループ処理]——–>[完了処理]–
Workerスレッド : └======[DB処理]======┘
非同期処理を実現するには、以下の2つのアプローチが存在する。
1. ポーリング方式(`State`プロパティ監視 + Win32 API制御):シンプルなモジュール構造で堅牢に動作。
2. イベント駆動方式(`WithEvents`):クラスモジュールを用いたCOMイベント受容による高度な非同期設計。
—
2. 実装パターン1:Win32 APIを用いた超高効率ポーリング非同期処理
単純に `DoEvents` をループ内で無制限に回すと、CPU使用率が100%に跳ね上がり、二次的なUIフリーズやメッセージの再入問題(Re-entrancy)を引き起こす。
これを回避するため、Win32 APIの `Sleep` とメッセージキュー状態の検知を組み合わせた高度なノンブロッキング・ポーリングループを構築する。
標準モジュール:`mod_AsyncExecute.bas`
64bit/32bit両対応(VBA7/Win64)のAPI定義を含めた完全実装。
Option Explicit
‘ ==============================================================================
‘ Win32 API Declarations (64bit / 32bit Safe)
‘ ==============================================================================
If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Private Declare PtrSafe Function GetQueueStatus Lib “user32” (ByVal flags As Long) As Long
Else
Private Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Private Declare Function GetQueueStatus Lib “user32” (ByVal flags As Long) As Long
End If
‘ GetQueueStatus 用定数(入力イベントまたは送信メッセージの有無を判定)
Private Const QS_ALLINPUT As Long = &H4FF
‘ ADO 定数の宣言(参照設定を行わない基本の定数定義)
Public Const adStateClosed As Long = 0
Public Const adStateOpen As Long = 1
Public Const adStateExecuting As Long = 4
Public Const adAsyncExecute As Long = &H10
Public Const adCmdText As Long = 1
‘ ==============================================================================
‘ 概要: 非同期でクエリを実行し、UIフリーズを防ぎながらプログレスを表示する
‘ ==============================================================================
Public Sub ExecuteQueryAsync_Polling()
On Error GoTo ErrorHandler
Dim conn As Object
Dim cmd As Object
Dim strConn As String
Dim strSQL As String
Dim startTime As Double
‘ 接続文字列(環境に合わせて変更してください)
‘ SQL Server Native Client / OLE DB Driver for SQL Server 推奨
strConn = “Provider=MSOLEDBSQL;Data Source=YOUR_SERVER;Initial Catalog=YOUR_DB;Integrated Security=SSPI;”
‘ 実行に時間がかかる重厚なSQL(例: 集計処理や大量更新)
strSQL = “WAITFOR DELAY ’00:00:08’; SELECT COUNT() FROM BigLogTable;”
‘ 1. COMオブジェクトの生成
Set conn = CreateObject(“ADODB.Connection”)
Set cmd = CreateObject(“ADODB.Command”)
‘ 2. コネクションオープン(同期)
conn.Open strConn
‘ 3. コマンドオブジェクトの設定
With cmd
Set .ActiveConnection = conn
.CommandText = strSQL
.CommandType = adCmdText
.CommandTimeout = 300 ‘ 5分タイムアウト
‘ 非同期フラグを付与して実行(即座に制御が返ってくる)
StatusBar “バックグラウンドでクエリを実行中…”
.Execute , , adAsyncExecute
End With
‘ 4. ノンブロッキング・ポーリングループ
startTime = Timer
Do While (cmd.State And adStateExecuting) <> 0
‘ メッセージキューに描画や操作(マウス・キー)イベントがある場合のみDoEventsを叩く
‘ これによりCPU使用率の爆発と再入問題を極限まで抑える
If GetQueueStatus(QS_ALLINPUT) <> 0 Then
DoEvents
End If
‘ UIプログレス表示の更新(例: 経過時間をステータスバーへ)
SysCmd acSysCmdSetStatus, “実行中… 経過時間: ” & Format$(Timer – startTime, “0.0”) & ” 秒”
‘ CPUコアを過剰に占有させないために10msのスリープを挿入
Sleep 10
Loop
‘ 5. 実行完了後の結果処理
SysCmd acSysCmdClearStatus
MsgBox “非同期クエリの実行が完了しました。(処理時間: ” & Format$(Timer – startTime, “0.0”) & ” 秒)”, vbInformation, “完了”
CleanUp:
‘ 明示的なオブジェクト解放(メモリリーク防止の絶対条件)
On Error Resume Next
If Not cmd Is Nothing Then Set cmd = Nothing
If Not conn Is Nothing Then
If conn.State <> adStateClosed Then conn.Close
Set conn = Nothing
End If
Exit Sub
ErrorHandler:
SysCmd acSysCmdClearStatus
MsgBox “エラー発生: [” & Err.Number & “] ” & Err.Description, vbCritical, “システムエラー”
Resume CleanUp
End Sub
‘ 簡易ステータスバー出力ヘルパー
Private Sub StatusBar(ByVal Msg As String)
SysCmd acSysCmdSetStatus, Msg
End Sub
—
3. 実装パターン2:`WithEvents`を用いた完全イベント駆動型アーキテクチャ
単なるループによる監視すら排除し、COMインターフェースからの通知を受け取る完全非同期イベント駆動モデルを構築する。このパターンは、フォームUIとバックグラウンド通信をスマートに分離(デカップリング)する際に最も威力を発揮する。
1. クラスモジュール:`clsAsyncQuery.cls`
Option Explicit
‘ ADOコネクションのイベントを直接キャッチする宣言
Private WithEvents m_conn As ADODB.Connection
‘ パブリックイベント定義(呼び出し元のフォームやモジュールへ通知)
Public Event QueryComplete(ByVal RecordsAffected As Long, ByVal ExecutionTime As Double)
Public Event QueryError(ByVal ErrorNumber As Long, ByVal ErrorDescription As String)
Public Event ProgressUpdate(ByVal ElapsedSeconds As Double)
Private m_startTime As Double
Private m_isExecuting As Boolean
Public Property Get IsExecuting() As Boolean
IsExecuting = m_isExecuting
End Property
‘ ==============================================================================
‘ 実行開始メソッド
‘ ==============================================================================
Public Sub ExecuteSQLAsync(ByVal ConnectionString As String, ByVal SQL As String)
On Error GoTo Err_Handler
If m_isExecuting Then
Err.Raise vbObjectError + 1001, , “既存の非同期処理が完了していません。”
End If
Set m_conn = New ADODB.Connection
m_conn.ConnectionString = ConnectionString
m_conn.Open
m_startTime = Timer
m_isExecuting = True
‘ ConnectionオブジェクトのExecuteメソッドを非同期で実行
m_conn.Execute SQL, , adAsyncExecute
Exit Sub
Err_Handler:
m_isExecuting = False
RaiseEvent QueryError(Err.Number, Err.Description)
End Sub
‘ ==============================================================================
‘ ADO Connectionの非同期完了イベント(COMコールバック)
‘ ==============================================================================
Private Sub m_conn_ExecuteComplete(ByVal RecordsAffected As Long, _
ByVal pError As ADODB.Error, _
adStatus As ADODB.EventStatusEnum, _
ByVal pCommand As ADODB.Command, _
ByVal pRecordset As ADODB.Recordset, _
ByVal pConnection As ADODB.Connection)
m_isExecuting = False
‘ ステータス判定
If adStatus = adStatusErrorsFailed Then
If Not pError Is Nothing Then
RaiseEvent QueryError(pError.Number, pError.Description)
Else
RaiseEvent QueryError(-1, “不明な非同期実行エラーが発生しました。”)
End If
Else
Dim elapsedTime As Double
elapsedTime = Timer – m_startTime
RaiseEvent QueryComplete(RecordsAffected, elapsedTime)
End If
‘ リソースの即時解放
On Error Resume Next
If pConnection.State = adStateOpen Then pConnection.Close
End Sub
2. 呼び出し側:フォームモジュール(`Form_frmMain.doccls`)
フォーム上に「実行ボタン(`cmdRun`)」と「状態表示用ラベル(`lblStatus`)」が存在する前提。
Option Explicit
‘ クラスのインスタンスをWithEventsで保持
Private WithEvents m_asyncQuery As clsAsyncQuery
Private Sub Form_Load()
Set m_asyncQuery = New clsAsyncQuery
End Sub
Private Sub cmdRun_Click()
Dim strConn As String
Dim strSQL As String
strConn = “Provider=MSOLEDBSQL;Data Source=YOUR_SERVER;Initial Catalog=YOUR_DB;Integrated Security=SSPI;”
strSQL = “UPDATE LargeTable SET ProcessedFlag = 1 WHERE ProcessedFlag = 0;”
Me.cmdRun.Enabled = False
Me.lblStatus.Caption = “処理を実行中… 他の操作が可能です。”
‘ 非同期実行開始(処理は即座に呼び出し元に戻る)
m_asyncQuery.ExecuteSQLAsync strConn, strSQL
End Sub
‘ — クラスからのイベントをキャッチする各種ハンドラ —
Private Sub m_asyncQuery_QueryComplete(ByVal RecordsAffected As Long, ByVal ExecutionTime As Double)
Me.lblStatus.Caption = “完了: ” & RecordsAffected & ” 件更新 (” & Format$(ExecutionTime, “0.00”) & “秒)”
Me.cmdRun.Enabled = True
MsgBox “バックグラウンド処理が正常に終了しました。”, vbInformation
End Sub
Private Sub m_asyncQuery_QueryError(ByVal ErrorNumber As Long, ByVal ErrorDescription As String)
Me.lblStatus.Caption = “エラー発生”
Me.cmdRun.Enabled = True
MsgBox “クエリエラーが発生しました [” & ErrorNumber & “]: ” & ErrorDescription, vbCritical
End Sub
Private Sub Form_Unload(Cancel As Integer)
‘ 画面終了時に未完了の実行があれば参照を安全に切断
If Not m_asyncQuery Is Nothing Then
If m_asyncQuery.IsExecuting Then
MsgBox “処理がバックグラウンドで実行中のため閉じることはできません。”, vbExclamation
Cancel = True
Exit Sub
End If
Set m_asyncQuery = Nothing
End If
End Sub
—
4. 現場で直面する落とし穴と極限の最適化
`adAsyncExecute` は極めて強力だが、STAアーキテクチャ上で安全に運用するためには、プロバイダ依存の特性やCOMのライフサイクルに対する深い洞察が不可欠である。
① OLE DB / ODBC プロバイダの非同期サポート検証
すべてのデータベースプロバイダが `adAsyncExecute` を正しくサポートしているわけではない。
- MSOLEDBSQL (Microsoft OLE DB Driver for SQL Server):完全サポート(最高性能)。
- ACE.OLEDB (Access / Excel):ローカルMDB/ACCDBに対する非同期実行は内部的に同期化されるか、制限を受ける場合がある。
- MySQL/PostgreSQL ODBC:ドライバのバージョン(特に32bit/64bitの差異)によって挙動が不安定になるケースがあるため、本番投入前に `adStateExecuting` の状態推移をログで確認すること。
② トランザクション処理における非同期の禁忌
`BeginTrans` を実行した同一コネクション上で `adAsyncExecute` を発行する場合、「トランザクションのスコープがWorkerスレッド間でどのように伝播するか」に留意しなければならない。基本原則として、非同期実行中のコネクションに対してコミット(`CommitTrans`)やロールバック(`RollbackTrans`)を試みてはならない。必ず `adStateExecuting` が解除されたことを確認した後にトランザクション操作を行わなければ、COMコンテキストの不整合により不可解な例外(エラーコード `0x80040E4D` 等)が発生する。
③ メモリリークを完全に潰す「参照の明示的破棄」
VBAの参照カウント機構(COM Garbage Collection)は、`WithEvents` で結びつけられた循環参照によって解放漏れを起こしやすい。
非同期処理が完了した際、あるいはエラー発生時には、必ず以下を厳守せよ。
‘ 明示的なイベントハンドラの切断とオブジェクト破棄
Set m_conn = Nothing
Set m_asyncQuery = Nothing
これを怠ると、Accessのプロセス(`MSACCESS.EXE`)がバックグラウンドに残留し、メモリを喰い潰す重大な障害へと発展する。
—
5. 結論:レガシーを「現代的アーキテクチャ」へ昇華させる
「Accessは遅い」「画面が固まるから使えない」という批判の9割は、単に開発者がI/Oの同期性とCOM STAの制御メカニズムを理解していないことに起因する。
ADOの `adAsyncExecute` と非同期ポーリング、Win32 APIメッセージルーティング、そして `WithEvents` によるコンポーネント化を正しく適用すれば、Access VBAシステムは現代的なマルチスレッドアプリケーションと同等のレスポンス性能を獲得する。
コードのライフサイクルを見極め、メモリ管理を徹底し、アーキテクチャの極限性能を引き出すこと。これこそが、レガシーシステムを保守・進化させ続けるシニアアーキテクチャの命題である。
