【テクニカル・上級編】DoCmd.RunCommandによる「レコードの保存」強制とバリデーションのタイミング制御 – Access VBA解析バイブル

スポンサーリンク

DoCmd.RunCommandによる「レコードの保存」強制とバリデーションのタイミング制御

Access VBAによる業務システム開発において、最も多くのエンジニアを泥沼に引きずり込む元凶の一つが「レコードの更新とバリデーションのタイミング制御」である。

フォーム上のコントロールに入力された値が、いつバッファからテーブルへ書き込まれるのか。そのライフサイクルを完全に掌握していなければ、いわゆる「保存の競合(Write Conflict)」や、意図しないタイミングでのバリデーションエラー、さらには「保存されたつもりでデータが消えていた」という致命的なデータロストを引き起こす。

今回は、Accessのオブジェクトモデルの深層に踏み込み、`DoCmd.RunCommand acCmdSaveRecord` を用いて強制的にデータを確定させ、バリデーションのタイミングを完全に支配するための極限の知見を授ける。

1. Accessフォームの更新ライフサイクルと「保存の罠」

まず、Accessがレコードを保存するメカニズムの基本を再確認する。多くの初学者は、フォームのテキストボックスからフォーカスが外れた瞬間や、別のレコードに移動した瞬間にデータが物理的に保存されると誤解している。

実際には、Accessのフォームはメモリ上に「ダーティ(Dirty)」な状態の編集バッファを持っており、以下のイベントチェーンを経て初めてデータベースエンジン(ACE)にコミットされる。

1. BeforeUpdate(フォーム): レコードがディスク/テーブルに書き込まれる直前に発生。ここでバリデーションを行い、`Cancel = True` を返せば保存を阻止できる。
2. AfterUpdate(フォーム): 書き込みが完了した直後に発生。
3. Current(フォーム): レコードのポインタが移動した際に発生。

問題は、「ユーザーが明示的に保存ボタンを押したとき」や「別の処理へ移行する直前」に、このダーティバッファが確実にコミットされている保証がない点にある。特に、VBAから他のテーブルを直接操作する処理(DAO/ADO)を挟む場合、フォーム上の未保存データとの間で「保存の競合」が誘発される。

2. なぜ `Me.Dirty = False` では不十分なのか?

フォームの未保存状態を強制的に保存させるためのイディオムとして、`Me.Dirty = False` がよく使われる。これは「現在編集中のレコードが変更されている(Dirtyである)というフラグを解除する」ことで、強制的にフォームの更新処理(BeforeUpdate → AfterUpdate)を走らせるテクニックだ。

しかし、シニアアーキテクトの視点から言えば、`Me.Dirty = False` は万能ではない

コントロールにフォーカス当たったまま、あるいは入力値の検証(IMEの確定待ち状態など)が完全に終わっていない不安定なコンテキストにおいて `Me.Dirty = False` を実行すると、Accessの内部エンジンが混乱し、ランタイムエラー(エラー番号:2115や3197など)を吐くか、最悪の場合、サイレントに保存がスキップされる。

ここで投入すべきなのが、AccessのUIコマンドを直接叩く `DoCmd.RunCommand acCmdSaveRecord` である。

3. `DoCmd.RunCommand acCmdSaveRecord` による完全な同期制御

`acCmdSaveRecord` は、AccessのUI層に対して「今すぐアクティブなレコードを保存せよ」というコマンドを直接発行する。これにより、Accessの内部メッセージループとフォームの更新イベントが強制的に同期され、いかなる状況下でも確実にトランザクションの境界を作る事ができる。

以下のコードは、ボタンクリック時に確実にデータを保存し、その直後に独自の厳密なバリデーションとトランザクション処理を実行する実用的なパターンである。

Private Sub cmdSaveAndProcess_Click()
On Error GoTo ErrorHandler

‘ 1. エラートラップを考慮し、コントロールの入力を確実に確定させる
‘ 入力中のコントロールがある場合、一度フォーカスを安全な場所(Detail等)へ逃がすテクニック
Me.A_Safe_ControlName.SetFocus

‘ 2. DoCmd.RunCommandによる強制保存の実行
‘ ここでフォームの BeforeUpdate / AfterUpdate イベントが同期的に発火する
If Me.Dirty Then
DoCmd.RunCommand acCmdSaveRecord
End If

‘ 3. 保存が成功した後に実行する独自のビジネスロジック(DAOトランザクション等)
Dim db As DAO.Database
Set db = CurrentDb

db.BeginTrans

‘ 例:子テーブルへの連動処理や複雑な集計更新
db.Execute “UPDATE T_SubTable SET Status = 1 WHERE ParentID = ” & Me.txtID.Value, dbFailOnError

db.CommitTrans

MsgBox “データの保存と処理が正常に完了しました。”, vbInformation, “システム通知”
Exit Sub

ErrorHandler:
‘ トランザクション中のエラー発生時はロールバック
If Not db Is Nothing Then
‘ DAOのトランザクション中であればロールバックを検討
‘ ※CurrentDbのスコープに注意
End If

Select Case Err.Number
Case 2046
‘ acCmdSaveRecordが実行できない状況(すでに保存されている等)は無視
Resume Next
Case 2115
MsgBox “入力されたデータが無効です。コントロールの内容を確認してください。”, vbCritical, “検証エラー”
Case Else
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical, “致命的エラー”
End Select
End Sub

4. バリデーションタイミングの制御:フォームイベント vs コード内制御

堅牢なシステムを構築する場合、「どこでバリデーションを行うべきか」の設計が命運を分ける。

悪手:コントロールの Exit イベントでの個別バリデーション

各コントロールの `Exit` イベントで値の妥当性をチェックし `Cancel = True` を返す手法は、ユーザーがフォーム全体の入力を放棄して閉じようとした際などにイベント順序の迷宮に入り込み、フォームが閉じられなくなるバグの温床となる。

奨励されるアプローチ:フォームの `BeforeUpdate` イベントへの集約

すべての入力検証は、フォームの `BeforeUpdate` イベントに集約すべきである。
`DoCmd.RunCommand acCmdSaveRecord` を呼び出すと、必ずこのフォームの `BeforeUpdate` が通るため、保存前の最後の砦として機能する。

Private Sub Form_BeforeUpdate(Cancel As Integer)
‘ —————————————————————-
‘ フォーム全体の保存前バリデーション(最終防衛ライン)
‘ —————————————————————-

‘ 1. 必須項目の網羅的チェック
If IsNull(Me.txtCustomerName) Or Trim(Me.txtCustomerName) = “” Then
MsgBox “顧客名は必須入力です。”, vbExclamation, “入力漏れ”
Me.txtCustomerName.SetFocus
Cancel = True
Exit Sub
End If

‘ 2. 業務ロジックに基づく整合性チェック(例:開始日と終了日の関係)
If Not IsNull(Me.txtStartDate) And Not IsNull(Me.txtEndDate) Then
If Me.txtStartDate > Me.txtEndDate Then
MsgBox “終了日は開始日以降の日付を指定してください。”, vbExclamation, “論理エラー”
Me.txtEndDate.SetFocus
Cancel = True
Exit Sub
End If
End If

‘ ここを通過した場合のみ、データはテーブルへ永続化される
End Sub

5. メモリ最適化とレガシー環境における注意点

Access VBAのオブジェクトモデル(特に `CurrentDb`)は、呼び出すたびに新しいデータベースインスタンスのオーバーヘッドが発生する。多層的なイベントやループ処理の中で無駄に `CurrentDb` を乱用すると、メモリリークやJet/ACEエンジンの内部キャッシュの肥大化を招く。

  • オブジェクト変数の明示的解放: DAOオブジェクト(`Database`, `Recordset`)を使用する際は、必ずローカル変数を宣言し、処理の最後には `Set xxx = Nothing` でメモリを解放する。
  • コンテキストの保持: `DoCmd.RunCommand` はUIスレッドに依存するため、非同期処理やバックグラウンドスレッド(そもそもAccess VBAではマルチスレッドは御法度だが)からの呼び出しは絶対に行わないこと。

総括

`DoCmd.RunCommand acCmdSaveRecord` は、単なる「保存ボタンの代用品」ではない。それは、不安定なUIバッファと厳格なデータベースエンジンとの間に橋を架け、データの整合性を担保するための極めて強力な同期プリミティブである。

このライフサイクルとタイミングの制御を完全にマスターした者だけが、Accessというレガシーの皮を被った強力なラピッド開発環境を、ミッションクリティカルな現場で淀みなく操る資格を持つ。妥協なきコードで、システムを極限まで最適化せよ。

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