1. Accessの「暗黙のコミット」という罠とDirtyプロパティの真実
Microsoft Accessにおける最大の罠、それは「ユーザーが明示的に保存操作をしなくても、データが勝手に保存される」という、Jet/ACEデータベースエンジン固有の暗黙のコミット(Auto-Save)仕様です。
ユーザーがテキストボックスの値を書き換え、そのまま次のレコードへ移動する、あるいは右上の「×」ボタンでフォームを閉じる。この瞬間、Accessは開発者の意図、そしてユーザーの「保存するかどうか」の意思を無視して、容赦なくデータをテーブルに書き込みます。
この挙動を制御し、現代的なアプリケーションとして「未保存の変更があります。保存しますか?」というインターフェースを提供するための唯一の鍵が、フォームの`Dirty`(更新中)プロパティです。
Dirtyプロパティの本質
`Dirty`プロパティは、メモリ上の編集バッファと、ディスク上の物理データ(またはキャッシュ)の間に生じた「不一致(ブレ)」を示すブーリアン値です。
- `Me.Dirty = True`:メモリ上のバッファが書き換えられ、ディスク上のデータと乖離している状態。
- `Me.Dirty = False`:メモリ上のバッファがディスクに書き込まれた(コミットされた)、あるいは一切の変更がない状態。
重要なのは、「いつ、どのイベントでこのプロパティを評価するか」という、Accessオブジェクトモデルのライフサイクルに対する深い洞察です。多くの開発者がこのイベントシーケンスを誤解し、データ破壊や、無限イベントループによるクラッシュを引き起こしています。
—
2. Dirty監視のライフサイクル設計:イベント連鎖の絶対防衛線
未保存データの破棄・保存を制御するうえで、最も犯しやすい間違いは「`Form_Unload` や `Form_Close` イベントで `Me.Dirty` を判定しようとすること」です。
これは完全に手遅れです。Accessのライフサイクルにおいて、フォームが閉じる、あるいはレコードが移動する際、`BeforeUpdate`(更新前処理)イベントは、`Unload` や `Close` よりも遥か前に実行されます。
フォーム終了・レコード移動時のイベント実行順序
[ユーザーの操作 (閉じる/移動)]
│
▼
1. Form_BeforeUpdate <-- ★ここが唯一の絶対防衛線。ここでCancel=Trueにしないと書き込まれる
│
2. Form_AfterUpdate (書き込み完了)
│
3. Form_Unload (ここでDirtyを判定しても、既にDirty=Falseになっている)
│
4. Form_Close
つまり、未保存データの制御における主戦場は`Form_BeforeUpdate`以外に存在しません。このイベントの引数である `Cancel` を `True` に設定することで、初めてAccessの暗黙のコミットを阻止することができます。
さらに、業務システムにおいては「Windowsの『×』ボタンによる強制終了」や「Alt + F4」といった、フォーム外からの割り込み破壊操作に対しても堅牢でなければなりません。
—
3. 極限の堅牢性を備えたDirty制御実装コード
以下に、実稼働環境での運用に耐えうる、極めて堅牢なDirty制御の実装を示します。
このコードは、単にメッセージボックスを出すだけでなく、以下の高度な制御を行います。
1. Win32 APIによる「×」ボタン(閉じるボタン)の動的制御:ユーザーが不用意にフォームを閉じられないよう、未保存時は閉じる操作をトラップします。
2. イベントの多重評価防止(再帰呼び出しの抑止):`Undo`処理実行時に発生する二次的なイベント連鎖をフラグで制御します。
3. エラーハンドリングとメモリ解放の徹底。
標準モジュール:`mod_FormController`
まずは、Windows APIを用いて、フォームの「閉じる」ボタンをAPIレベルで制御(または無効化)するためのヘルパー関数を定義します。
Option Compare Database
Option Explicit
‘ Windows API 宣言 (32bit/64bit 互換 VBA7)
If VBA7 Then
Private Declare PtrSafe Function GetWindowLong Lib “user32” Alias “GetWindowLongA” (ByVal hwnd As LongPtr, ByVal nIndex As Long) As Long
Private Declare PtrSafe Function SetWindowLong Lib “user32” Alias “SetWindowLongA” (ByVal hwnd As LongPtr, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Private Declare PtrSafe Function DrawMenuBar Lib “user32” (ByVal hwnd As LongPtr) As Long
Else
Private Declare Function GetWindowLong Lib “user32” Alias “GetWindowLongA” (ByVal hwnd As Long, ByVal nIndex As Long) As Long
Private Declare Function SetWindowLong Lib “user32” Alias “SetWindowLongA” (ByVal hwnd As Long, ByVal dwNewLong As Long) As Long
Private Declare Function DrawMenuBar Lib “user32″ (ByVal hwnd As Long) As Long
End If
Private Const GWL_STYLE As Long = -16
Private Const WS_SYSMENU As Long = &H80000
”’
”’
Public Sub SetFormCloseButton(ByVal frmHWnd As LongPtr, ByVal Enabled As Boolean)
Dim lngStyle As Long
On Error GoTo Error_Handler
lngStyle = GetWindowLong(frmHWnd, GWL_STYLE)
If Enabled Then
lngStyle = lngStyle Or WS_SYSMENU
Else
lngStyle = lngStyle And Not WS_SYSMENU
End If
Call SetWindowLong(frmHWnd, GWL_STYLE, lngStyle)
Call DrawMenuBar(frmHWnd)
Exit_Point:
Exit Sub
Error_Handler:
‘ 本番環境のサイレント障害を防ぐため、ログ出力を推奨
Debug.Print “Error in SetFormCloseButton: ” & Err.Description
Resume Exit_Point
End Sub
フォームモジュール:`Form_F_CustomerDetail`
次に、監視対象となるメインフォーム(例:顧客詳細入力フォームなど)のコードビハインドです。
Option Compare Database
Option Explicit
‘ フォームの状態を管理する内部変数
Private m_isUndoing As Boolean ‘ Undo処理中のイベント再帰防止フラグ
Private m_allowClose As Boolean ‘ 独自の「閉じる」ボタン経由の終了かを識別するフラグ
”’
”’
Private Sub Form_Load()
On Error GoTo Error_Handler
m_isUndoing = False
m_allowClose = False
‘ 起動時はAPIを使用して、Access標準の「×」ボタンによる予期せぬ終了を制限する
‘ (ユーザーにはフォーム上に配置した「保存」「閉じる」ボタンの使用を強制)
Call SetFormCloseButton(Me.hwnd, False)
Exit_Point:
Exit Sub
Error_Handler:
MsgBox “初期化処理でエラーが発生しました: ” & Err.Description, vbCritical, “システムエラー”
Resume Exit_Point
End Sub
”’
”’
Private Sub Form_BeforeUpdate(Cancel As Integer)
Dim lngResult As VbMsgBoxResult
‘ Undo実行中の場合は、余計な判定を通さず抜ける
If m_isUndoing Then Exit Sub
On Error GoTo Error_Handler
‘ Dirtyプロパティを明示的に評価
If Me.Dirty Then
‘ ユーザーへの意思確認
lngResult = MsgBox(“データに変更が加えられています。” & vbCrLf & _
“この変更をデータベースに保存しますか?” & vbCrLf & vbCrLf & _
“[はい] : 変更を保存して処理を続行します。” & vbCrLf & _
“[いいえ] : 変更を破棄(ロールバック)します。” & vbCrLf & _
“[キャンセル] : 編集画面に戻ります。”, _
vbQuestion + vbYesNoCancel + vbDefaultButton3, “未保存データの確認”)
Select Case lngResult
Case vbYes
‘ 「はい」の場合、Access本来の書き込み処理を続行させるため、何もしない
‘ (BeforeUpdateを抜けた後、自動的に物理保存される)
Case vbNo
‘ 「いいえ」の場合、現在の変更を完全にロールバックする
m_isUndoing = True
Me.Undo
m_isUndoing = False
‘ Undoを実行したため、BeforeUpdateの「更新処理自体」はキャンセルする
Cancel = True
Case vbCancel
‘ 「キャンセル」の場合、編集画面に戻すため、更新処理をキャンセル
Cancel = True
‘ 画面を閉じる処理の途中であった場合は、閉じる動作もキャンセルさせる
m_allowClose = False
End Select
End If
Exit_Point:
Exit Sub
Error_Handler:
m_isUndoing = False
MsgBox “更新監視処理中にエラーが発生しました: ” & Err.Description, vbCritical, “エラー”
Cancel = True ‘ 安全のため、エラー時は書き込みを強制ブロック
Resume Exit_Point
End Sub
”’
”’
Private Sub Form_Unload(Cancel As Integer)
On Error GoTo Error_Handler
‘ 独自の「閉じる」ボタンを介さず、かつDirty状態が残っている場合はアンロードを拒否
If Not m_allowClose Then
If Me.Dirty Then
‘ BeforeUpdateが先に走るため、通常ここを通過することはないが、
‘ 予期せぬオブジェクトライフサイクルの乱れに対する多重防御壁として機能させる
Cancel = True
Exit Sub
End If
End If
‘ フォームが安全に閉じられる際、APIで変更したスタイルを復元する
Call SetFormCloseButton(Me.hwnd, True)
Exit_Point:
Exit Sub
Error_Handler:
Debug.Print “Error in Form_Unload: ” & Err.Description
Resume Exit_Point
End Sub
”’
”’
Private Sub cmdSave_Click()
On Error GoTo Error_Handler
If Me.Dirty Then
‘ 明示的に書き込みを実行(これによりForm_BeforeUpdateがトリガーされる)
Me.Dirty = False
MsgBox “データを保存しました。”, vbInformation, “完了”
Else
MsgBox “変更されたデータはありません。”, vbInformation, “通知”
End If
Exit_Point:
Exit Sub
Error_Handler:
MsgBox “保存処理中にエラーが発生しました: ” & Err.Description, vbCritical, “エラー”
Resume Exit_Point
End Sub
”’
”’
Private Sub cmdClose_Click()
On Error GoTo Error_Handler
‘ 1. まず現在のDirty状態を明示的に解決しに行く
If Me.Dirty Then
‘ Dirty=Falseをセットすることで、Form_BeforeUpdateを強制的に走らせる
‘ ユーザーが「キャンセル」を選択した場合、BeforeUpdateがキャンセルされ、Me.Dirty=Trueのまま残る
Me.Dirty = False
End If
‘ 2. BeforeUpdateでのユーザーの選択結果(Dirtyが解消されたか)を評価
If Not Me.Dirty Then
m_allowClose = True
DoCmd.Close acForm, Me.Name, acSaveNo
End If
Exit_Point:
Exit Sub
Error_Handler:
‘ 2101 (Dirtyプロパティの設定エラー:BeforeUpdateでCancelされた場合など) は無視する
If Err.Number <> 2101 Then
MsgBox “閉じる処理中にエラーが発生しました: ” & Err.Description, vbCritical, “エラー”
End If
Resume Exit_Point
End Sub
—
4. レガシー環境の保守とメモリ管理:オブジェクト解放の極意
Accessシステムを20年以上にわたり塩漬けにされたレガシー環境、あるいは数万レコードを扱うハイパフォーマンス環境で保守する場合、上記のDirty制御だけでは不十分です。
フォームの背後で暗躍する「メモリリーク」と「オブジェクト参照のロック」を解決しなければ、ある日突然、Accessは「メモリ不足です」という悲鳴とともに強制終了します。
`CurrentDb` のキャッシュ問題と絶対参照の罠
Access VBAで最も多用され、かつ最も誤解されているのが `CurrentDb` メソッドです。
‘ ★最悪のアンチパターン
Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset(“SELECT FROM T_Customer”)
`CurrentDb` はプロパティではなくメソッドです。呼び出されるたびに、Jet/ACEデータベースエンジンの新しいインスタンス(Databaseオブジェクト)を背後で生成します。上記のように直接チェーンさせてオブジェクトを生成すると、内部的にインスタンスが解放されず、メモリリークを誘発します。
Dirty制御と連動してマスタデータの検証などを行う場合は、必ず以下のように親オブジェクトから明示的に解放してください。
”’
”’
Public Function IsCodeDuplicate(ByVal targetCode As String) As Boolean
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim isDup As Boolean
isDup = False
On Error GoTo Error_Handler
‘ インスタンスを明示的に変数に格納(ライフサイクルの管理)
Set db = CurrentDb
‘ 読み取り専用・前方スクロールのみ(最速のカーソルタイプ)で開く
Set rs = db.OpenRecordset(“SELECT CustomerID FROM T_Customer WHERE CustomerCode = ‘” & Replace(targetCode, “‘”, “””) & “‘”, _
dbOpenForwardOnly, dbReadOnly)
If Not rs.EOF Then
isDup = True
End If
Exit_Point:
‘ 【重要】LIFO(Last In, First Out)の原則に従い、下位オブジェクトから確実に閉じて解放する
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
If Not db Is Nothing Then
‘ db.Close は不要(CurrentDbオブジェクトはCloseすると不具合を起こす場合があるため、参照剥がしのみ実行)
Set db = Nothing
End If
IsCodeDuplicate = isDup
Exit Function
Error_Handler:
‘ エラー時も絶対にメモリをリークさせない執念の解放処理
Debug.Print “Error in IsCodeDuplicate: ” & Err.Description
Resume Exit_Point
End Function
—
5. アーキテクトとしての設計思想
Access VBAは、その手軽さゆえに「誰でも書けるスパゲッティコード」の温床とみなされがちです。しかし、プラットフォームとしてのAccess、そしてJet/ACEエンジンは、極めて緻密なライフサイクルに基づいて動作しています。
今回解説した `Dirty` プロパティの制御は、単なる「確認メッセージの表示」ではありません。Accessの「暗黙の物理書き込み」というデータベースエンジン直結の挙動に対し、メモリバッファの段階でトランザクションに類似した防壁(Undo)を築く、極めて高度なアーキテクチャ設計です。
Windows APIを用いたUIの制御、オブジェクトライフサイクルに則ったイベントの厳密なハンドリング、そして徹底したメモリ解放。これら一連の作法を徹底して初めて、Accessシステムは「レガシーの遺物」から「エンタープライズに耐えうる堅牢なフロントエンド基盤」へと昇華します。
技術の細部に宿る真実に目を向け、泥臭いレガシー環境を最高の芸術へと仕立て上げてください。
