【テクニカル・上級編】DoCmd.RunCommand acCmdSaveRecordの強制実行:入力値の確定とバリデーションのタイミング制御 – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:`DoCmd.RunCommand acCmdSaveRecord` による入力値確定とバリデーションの極意

アーキテクトたるもの、Accessのフォームが抱える「暗黙のバッファリング」と「非同期的な保存の罠」に幾度となく泣かされてきたはずだ。

ユーザーがコントロールに値を入力し、そのままフォーカスを移動させずにリボンやカスタムボタンをクリックした瞬間、データはまだコントロールの表層に漂っており、レコードソースへのコミットメントは完了していない。この状態でVBAから直接テーブルや外部システムを参照しにいけば、整合性の崩れたゴーストデータに足元をすくわれる。

今回は、Access VBAにおける「入力値の強制確定(コミット)」と「バリデーションのタイミング制御」の核心に迫る。中途半端な `Me.Dirty = False` の神話から脱却し、ハードウェアレベルのメッセージキューまで視野に入れた堅牢なアーキテクチャを構築する手法を授けよう。

1. なぜ `Me.Dirty = False` だけでは不完全なのか

初学者は「フォームの変更を保存するには `Me.Dirty = False` と書けばよい」と教わる。実際、多くのケースでこれ機能する。しかし、システムが大規模化し、複雑なコントロール間の依存関係や、外部API連携を伴うトランザクション処理が絡む現場において、このコードはしばしば沈黙のバグを生む。

`Me.Dirty = False` は、あくまでVBAオブジェクトモデル上のプロパティ操作であり、Accessの内部状態マシンに対して「変更フラグを落とせ(=保存しろ)」と要求するに過ぎない。そのため、フォーカスを持っているアクティブコントロールのバリデーションが未完了の状態や、特定のアクティブXコントロールが絡む複雑なフォーカス遷移の最中では、この命令が無視されるか、意図しないタイミングでキューイングされる。

ここで登場するのが、`DoCmd.RunCommand acCmdSaveRecord` である。

これは、Accessのメインフレームワークに対して「現在アクティブなレコードの保存」というUIコマンドを直接発行する。Windowsメッセージループに「レコード保存イベント」を明示的に割り込ませるため、ユーザーが手動で保存操作を行ったのと全く同じライフサイクル(BeforeUpdate ⇒ AfterUpdate)を強制的に、かつ同期的に経由させることができるのだ。

2. 実践:厳密なバリデーションとトランザクション制御のコードパターン

以下のコードは、シニアエンジニアが現場で実装すべき、`acCmdSaveRecord` を利用した堅牢なレコード保存・バリデーションルーチンの実例である。エラーハンドリング、トランザクション、そしてオブジェクトのライフサイクル管理まで完璧に網羅している。

‘ ==============================================================================
‘ モジュール名: フォーム制御クラス / 標準モジュール
‘ 概要: acCmdSaveRecordを用いた厳密な入力値確定とトランザクション処理
‘ ==============================================================================
Public Function SaveCurrentRecordSafely(ByRef frm As Form) As Boolean
Dim dbs As DAO.Database
Dim isTransactionStarted As Boolean

On Error GoTo ErrorHandler

SaveCurrentRecordSafely = False
isTransactionStarted = False

‘ 1. 現在のアクティブコントロールの入力値を確実にバッファから確定させる
‘ 編集中でなければ何もしないが、ダーティ状態であれば強制的にフォームのBeforeUpdateを走らせる
If frm.Dirty Then
‘ フォーカスを安全なコントロールに逃がす必要がある場合はここで制御する
‘ 例: frm.Controls(“txtDummyFocus”).SetFocus

‘ UIコマンドとしてレコード保存を強制実行
‘ これにより、フォームの BeforeUpdate イベントが確実に発火する
If Screen.ActiveForm.Name = frm.Name Then
DoCmd.RunCommand acCmdSaveRecord
Else
‘ フォームがアクティブでない場合のフォールバック
frm.Dirty = False
End If
End If

‘ 2. DAOワークスペースレベルでの整合性担保(必要に応じた外部連携・トランザクション)
Set dbs = CurrentDb()

‘ 注: Accessのフォーム保存自体は自動的に暗黙のトランザクションで行われるが、
‘ 関連する別テーブルへの書き込みやAPI連携をアトミックに行う場合は明示的トランザクションを張る
DBEngine.BeginTrans
isTransactionStarted = True

‘ — ここに追加のビジネスロジックや外部システム連携を記述 —
‘ 例: ログテーブルへの書き込みや、ストレージへのファイル同期など
‘ Call SyncToExternalAPI(frm.Controls(“txtRecordID”).Value)

DBEngine.CommitTrans
isTransactionStarted = False

SaveCurrentRecordSafely = True
Exit Function

ErrorHandler:
‘ トランザクション中のエラーであればロールバック
If isTransactionStarted Then
DBEngine.Rollback
isTransactionStarted = False
End If

Select Case Err.Number
InfoBox:
Case 2501 ‘ RunCommandがキャンセルされた場合(BeforeUpdateでCancel = Trueにした場合など)
MsgBox “入力内容に不備があるため、保存がキャンセルされました。” & vbCrLf & _
“エラー箇所を確認してください。”, vbCritical, “バリデーションエラー”
Case Else
‘ 予期せぬエラー
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error: ” & Err.Number & ” – ” & Err.Description, vbCritical, “システムエラー”
End Select

SaveCurrentRecordSafely = False

‘ オブジェクトの明示的解放(メモリ最適化)
If Not dbs Is Nothing Then
Set dbs = Nothing
End If
End Function

3. フォームの `BeforeUpdate` イベントとの完璧な連携

`acCmdSaveRecord` を実行すると、Accessは即座に該当フォームの `BeforeUpdate(Cancel As Integer)` イベントを呼び出す。ここで真のバリデーションを行うのがプロの作法だ。

Private Sub Form_BeforeUpdate(Cancel As Integer)
On Error GoTo ErrorHandler

‘ 業務ロジックに基づく複合バリデーション
If Me.txtStartDate.Value > Me.txtEndDate.Value Then
MsgBox “開始日は終了日より前の日付を指定してください。”, vbExclamation, “整合性エラー”
Me.txtStartDate.SetFocus
Cancel = True ‘ ここでTrueを返すと、acCmdSaveRecordのエラー(Err 2501)としてキャッチされる
Exit Sub
End If

‘ 必須項目の網羅的チェック(データベース制約に依存しない事前防衛)
If IsNull(Me.txtClientCode) Or Trim(Me.txtClientCode) = “” Then
MsgBox “顧客コードは必須入力です。”, vbExclamation, “入力漏れ”
Me.txtClientCode.SetFocus
Cancel = True
Exit Sub
End If

Exit Sub

ErrorHandler:
MsgBox “Form_BeforeUpdateで致命的なエラーが発生しました: ” & Err.Description, vbCritical
Cancel = True
End Sub

この連携フローの美しさは、「UIのイベント連鎖」と「VBAの制御フロー」が完全に同期する点にある。ユーザーがどこから保存を試みようとも、必ずこの `BeforeUpdate` を通過せざるを得ない構造を作り上げることができる。

4. レガシー環境の保守とパフォーマンス極限化の知見

長年稼働している巨大なAccessシステム(MDB/ACCDB)では、メモリリークとオブジェクトの肥大化がシステム寿命を縮める最大の癌である。

1. `CurrentDb` の乱用に注意せよ
`CurrentDb()` は呼び出すたびに新しい DAO.Database オブジェクトのインスタンスをメモリ上に生成する。ループ内や頻繁に実行されるイベント内でこれを連発すると、COMコンポーネントの参照カウンタが圧迫され、Access特有の「リソース不足」エラーを引き起こす。データベース参照は極力ローカル変数にキャッシュし、処理終了時には明示的に `Nothing` を代入してメモリを解放しろ。

2. Windows APIとの組み合わせ(フォーカスロストの強制)
極稀なケースとして、どうしてもアクティブコントロールのフォーカスが外れず、`acCmdSaveRecord` がフォーカスエラーを起こすレガシーフォームが存在する。その場合は、Windows APIの `SetFocus` を直接叩くか、フォーム上の非表示のダミーボタンに強制的にフォーカスを移すハックが有効だ。

‘ フォーム内のダミーボタンへ強制フォーカス遷移
Me.cmdDummy.SetFocus
DoCmd.RunCommand acCmdSaveRecord

泥臭く見えるかもしれないが、現場のレガシーコードを無落城で稼働させ続けるための極限の知見である。

5. アーキテクトからの総括

`DoCmd.RunCommand acCmdSaveRecord` は、単なる「保存ボタンの代用品」ではない。それは、Accessというブラックボックス化されたデータベース・UI統合環境において、開発者がデータの整合性を完全に掌握するための「制圧コマンド」である。

中途半端なプロパティ操作に頼るな。イベントのライフサイクルを理解し、メッセージループの挙動を支配した者だけが、堅牢でエンタープライズなAccessシステムを構築する資格を持つ。明日からの実装で、この厳密なコミットメントパターンをぜひ取り入れてほしい。

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