DAOからADOへの移行を掌握せよ:既存システムの堅牢性を守り抜くリファクタリングの極意
現場のエンジニア諸君、よく聞いてほしい。
Access VBAの開発において、多くの者が「DAO(Data Access Objects)」という、いわば「Access専用の庭」の中で安住している。ローカルのテーブルを操作する分には、DAOは極めて高速で、直感的だ。しかし、システムが成長し、データソースがSQL Serverへ、あるいは外部のWeb APIへと拡張を迫られたとき、その「庭」は瞬時に「檻」へと変わる。
そこで必要になるのが「ADO(ActiveX Data Objects)」への移行だ。
だが、安易な置換は死を招く。DAOとADOは、単なるライブラリの違いではない。「データへのアプローチ、オブジェクトのライフサイクル、そしてトランザクションの概念」が根本から異なる別物だ。
本稿では、既存の保守性を一切損なうことなく、ADOへと昇華させるためのアーキテクチャ設計と、プロフェッショナルが守るべき実装の鉄則を伝授する。
1. アーキテクチャの断絶:なぜ単純な置換では壊れるのか
移行に失敗する者が必ず陥る罠がある。それは「構文の書き換え」だけで終わってしまうことだ。
DAOの思想:エンジン直結型
DAOは、ACE(旧Jet)エンジンに密結合している。`Workspace` オブジェクトがトランザクションを管理し、`Database` オブジェクトがファイルへのパスを握る。これは「Accessというアプリケーションの内部機能」を操作している感覚に近い。
ADOの思想:プロバイダー・アブストラクション
対してADOは、OLE DBプロバイダーを介してデータにアクセスする。ADOにとって、接続先がAccessファイルであろうが、千里の彼方にあるSQL Serverであろうが、操作の抽象度は同じだ。
ここが重要だ:
DAOでは当たり前だった「レコードセットを操作しながら、同じデータベースに対して別の書き込みを行う」といった挙動が、ADO(特にサーバーサイドカーソルを使用する場合)ではロック競合や接続エラーを引き起こす原因となる。
2. 移行判断のクリティカル・チェックリスト
すべてのDAOコードをADOに変える必要はない。それはリソースの無駄だ。以下の基準で、移行の優先順位を決定せよ。
- [ ] データソースの外部化予定はあるか? (SQL Server, Azure SQL等への移行を見据えるなら、ADOへの移行は必須だ)
- [ ] 分散環境での動作が必要か? (ネットワーク越しに複数ユーザーが激しく書き込む場合、ADOの接続管理の方が制御しやすい)
- [ ] 複雑なSQL(JOINやサブクエリ)を多用しているか? (クライアント側での処理を減らし、サーバーサイドで実行させるにはADOの柔軟性が武器になる)
- [ ] 【移行不要】ローカルのAccessテーブルのみを、単純なCRUD操作で回している場合 (この場合は、速度面でDAOを維持する方が合理的だ)
3. 堅牢な設計のための3つの鉄則
ADOへ移行する際、君たちが守るべき「設計の規律」は以下の3点に集約される。
① 接続(Connection)のライフサイクルを制御せよ
ADOにおいて、`Connection` オブジェクトの管理は生命線だ。メソッドを呼ぶたびに `Open` し、終わるたびに `Close` するような、場当たり的な実装は絶対に許されない。接続のオーバーヘッドはパフォーマンスを劇的に悪化させる。
「接続は、ビジネスロジックの境界で一括管理する」。これが鉄則だ。
② カーソルタイプを戦略的に選択せよ
ADOには `adOpenStatic`, `adOpenKeyset`, `adOpenDynamic` といったカーソルタイプがある。
- Client-side Cursor: データをメモリに全ロードする。Access内での読み取り専用表示ならこれが最速だが、大量データではメモリを食いつぶす。
- Server-side Cursor: サーバー(またはエンジン)側でカーソルを保持する。大規模データには強いが、ネットワーク負荷とロックに注意が必要だ。
「とりあえず `adOpenStatic`」という思考停止は、プロの仕事ではない。
③ トランザクションの境界を明確にせよ
DAOの `Workspace.BeginTrans` と ADOの `Connection.BeginTrans` は、管理の単位が違う。ADOでは、「一つの接続(Connection)に対して一つのトランザクション」という意識を徹底せよ。
4. 実装の設計図:プロダクション品質のデータアクセス・クラス
では、実際に私が現場で導入する、堅牢なADOラップクラスの構造を見せよう。
単なる関数ではなく、「接続、エラーハンドリング、トランザクション」をカプセル化したクラスモジュールとして設計する。これにより、呼び出し側のコードは「データの出処」を意識せずに済む。
クラス名:`clsDataAccess` (Class Module)
Option Explicit
' =================================================================
' クラス名: clsDataAccess
' 概要: ADO接続とトランザクションをカプセル化する堅牢なデータアクセス層
' 設計思想: 接続のライフサイクル管理と、エラー発生時の自動ロールバックを保証する
' =================================================================
Private mConn As ADODB.Connection
Private mConnectionString As String
Private mInTransaction As Boolean
' 初期化時に接続文字列を保持
Public Sub Initialize(ByVal strConn As String)
mConnectionString = strConn
End Sub
' 接続の確立
Public Sub OpenConnection()
If mConn Is Nothing Then
Set mConn = New ADODB.Connection
End If
If mConn.State = adStateClosed Then
mConn.ConnectionString = mConnectionString
mConn.Open
End If
End Sub
' 接続の解放(必ず呼び出すこと)
Public Sub CloseConnection()
If Not mConn Is Nothing Then
If mConn.State = adStateOpen Then
mConn.Close
End If
Set mConn = Nothing
End If
mInTransaction = False
End Sub
' トランザクション開始
Public Sub BeginTransaction()
If mInTransaction Then Err.Raise vbObjectError + 513, "clsDataAccess", "既にトランザクションが開始されています。"
mConn.BeginTrans
mInTransaction = True
End Sub
' コミット
Public Sub CommitTransaction()
If Not mInTransaction Then Err.Raise vbObjectError + 514, "clsDataAccess", "開始されていないトランザクションをコミットしようとしています。"
mConn.CommitTrans
mInTransaction = False
End Sub
' ロールバック
Public Sub RollbackTransaction()
If mInTransaction Then
mConn.RollbackTrans
mInTransaction = False
End If
End Sub
' レコードセット取得(読み取り専用・高速化設定)
' 引数: strSQL - 実行するSQL
' 戻り値: ADODB.Recordset - クライアントサイドカーソルによる高速読み取り用
Public Function GetRecordset(ByVal strSQL As String) As ADODB.Recordset
Dim rs As New ADODB.Recordset
On Error GoTo ErrHandler
' 接続がなければ開く
If mConn Is Nothing Or mConn.State = adStateClosed Then Call OpenConnection
' 高速なクライアントサイドカーソルを利用した設定
' adOpenStatic + adLockReadOnly は、参照系処理において最も安定し、メモリ効率も良い
rs.CursorLocation = adUseClient
rs.Open strSQL, mConn, adOpenStatic, adLockReadOnly
Set GetRecordset = rs
Exit Function
ErrHandler:
' エラー発生時はレコードセットを破棄して上位へ投げる
Set rs = Nothing
Err.Raise Err.Number, "clsDataAccess.GetRecordset", Err.Description
End Function
' 更新系コマンドの実行(パラメータクエリ推奨)
Public Sub ExecuteNonQuery(ByVal strSQL As String)
On Error GoTo ErrHandler
If mConn Is Nothing Or mConn.State = adStateClosed Then Call OpenConnection
mConn.Execute strSQL
Exit Sub
ErrHandler:
Err.Raise Err.Number, "clsDataAccess.ExecuteNonQuery", Err.Description
End Function
' デストラクタ代わりのクリーンアップ
Private Sub Class_Terminate()
Call CloseConnection
End Sub
標準モジュールでの使用例(実務での実装イメージ)
このクラスを使うことで、メインのロジックは驚くほどクリーンになり、かつ堅牢になる。
Option Explicit
' 業務ロジック実行例
Public Sub ProcessOrderUpdate(ByVal OrderID As Long, ByVal NewStatus As String)
Dim db As New clsDataAccess
Dim rs As ADODB.Recordset
Dim strConn As String
' 接続文字列(本来は設定ファイルや隠し定数から取得すべき)
strConn = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\Data\MySystem.accdb;"
On Error GoTo ErrorHandler
' 1. 初期化と接続
db.Initialize strConn
db.OpenConnection
' 2. トランザクション開始
db.BeginTransaction
' 3. 更新処理(ExecuteNonQueryを使用)
db.ExecuteNonQuery "UPDATE Orders SET Status = '" & NewStatus & "' WHERE OrderID = " & OrderID
' 4. ログ記録(別のテーブルへの書き込み)
db.ExecuteNonQuery "INSERT INTO AuditLog (Action, TargetID) VALUES ('StatusUpdate', " & OrderID & ")"
' 5. 全て成功したらコミット
db.CommitTransaction
MsgBox "更新が正常に完了しました。", vbInformation
CleanUp:
' 6. 確実に接続を閉じる
Set db = Nothing
Exit Sub
ErrorHandler:
' 7. エラー時はロールバック
MsgBox "エラーが発生したため、処理を中断しロールバックしました。" & vbCrLf & _
"Error: " & Err.Description, vbCritical
db.RollbackTransaction
Resume CleanUp
End Sub
結論:アーキテクトの視点を持て
DAOからADOへの移行は、単なる「書き換え作業」ではない。それは、システムのデータアクセス層を、変化に強い「抽象化されたレイヤー」へと進化させる設計プロセスである。
今回提示したクラス設計のように、接続の管理、エラーハンドリング、トランザクションの制御を一つのオブジェクトに閉じ込める(カプセル化する)ことで、ビジネスロジックは純粋に「何をするか」に集中できるようになる。
「動けばいい」というコードを書くな。「変化に耐え、失敗しても整合性を保てる」コードを書け。それが、真にプロフェッショナルなエンジニアの仕事だ。
