Home  /  TOP  /  ABOUT  /  DTS  /  COMMIC  /  DEV  /  ARC  /  KNOWLEDGE  /  EVENTS  /  FORUMS  /  FAQ  /  LINK  /  ENGISH
ADO Tips

この項目のより詳しい情報検索のために
キーワード:「データアクセス」,「ADO」,「UpdateBatch」など
資料:MSDN,VBAヘルプ,プログラミングを扱っているHP,データアクセスを扱っているHP

ADOはMicrosoft社が提供する,データ(主としてリレーショナルデータベースのデータ)にプログラムからアクセスするためのオブジェクトです。
接続,レコードセット及びコマンドなどのオブジェクトを利用して,効果的にデータにアクセスすることができます。
現在では.NET Frameworkに統合されたADO.NETがメインステージになっていますが,VBA環境やASP環境ではまだまだADOのステージが残っていきます。
このテキストでは,ADOに関するいくつかの特徴的な挙動について解説しています。

動作確認環境
1 RecordSet.Filterの注意点

FilterプロパティはRecordsetオブジェクトで特定のレコードを絞り込むために利用します。

構文

Filterプロパティには検索文字列,ブックマーク配列,FilterGroupEnumの値から1つのいずれかを設定します。有効なプロパティ値が設定された時点で絞り込みが有効になり,AbsolutePosition,AbsolutePage,RecordCount,PageCountのプロパティ及びカレントカーソル位置が自動的に更新されます。

recordsetFilter = " 検索条件文字列 " [Or (Bookmark)] [Or FilterGroupEnum]

検索条件文字列

SQL文のWHERE句に相当する文字列を指定します。
但し,使用できる演算子は基本比較演算子(=,<,>,<=,=>,<>)とLIKEのみです(SQL文のIN演算子などは使用できません)。
LIKE使用時には比較する値にワイルドカード(*,%)が使えますが,両者に違いはなく複数文字列が一致します
また,ワイルドカードは値の最後にのみ使用できます。
(「LIKE '1*3'」など中間にワイルドカードを使用した場合は,該当なしで絞り込まれてしまい,エラーも返しません。「LIKE '1%'」などの後方複数文字列一致のみに利用できます)。

FilterとUpdateBatchの組み合わせ利用時の注意点

Filterで絞り込んだレコードに対して一連の更新を行い,バッチアップデートをかける要請があったとします。
この際,更新の状況によって発見しにくい論理的なバグが発生する可能性があります。

A 再現の条件
取得したレコードセットに検索文字列指定のFilterをかけ,検索条件に指定したフィールドの値を含むいくつかのフィールドを更新します。
この後Filterを解除しないでUpdateBatchメソッドを実行します。

B バグの現象
一部の更新がサーバーに送信されず,明確なエラーも発生しません。

C 解説
Filterプロパティによるレコードの絞り込みは常に動的に行われるため,カーソル移動のタイミングで常に絞り込みがリフレッシュされます
よって,検索条件に指定されたフィールドが検索条件から外れるような値に書き換えられた場合,次のカーソル移動のタイミングで更新がセットされたレコードは絞り込みレコードから除外されてしまいます
このことによって,Filterを解除する前にUpdateBatchメソッドを実行してしまうと,更新がセットされたレコードが絞り込みから外れている状態でUpdateBatchの処理が実行されるため,一部更新が送信されません
しかし,UpdateBatchメソッド自身は絞り込み対象のレコードに対する正常な処理を実行していると見なすために明確なエラーが発生することはありません
このことによって,この現象は潜在的な論理バグとなります。

D 対策
  • 更新対象のフィールドをFilterの検索条件に指定しない
    更新対象のフィールドを利用してFilterをかけない限り,この現象は再現しません。

  • UpdateBatchメソッドを実行する前にFilterを解除する
    更新対象フィールドを利用してFilterをかけざるを得ない場合は,更新のセットの間のみFilterを利用します。
    更新をセットした時点で対象レコードは絞り込みからはずれますが,更新自体は正常にセットされています
    UpdateBatchメソッドを呼び出す前に,Filterを解除して全件レコードをアクティブにすると,当初の要請どおり絞り込まれたレコードのみに更新がセットされたレコードセットが完成しています。
    ここでUpdateBatchを呼び出せば,全ての更新が正常にサーバーに送信されます。
    UpdateBatchメソッドは値の更新されていないレコードの情報を送信しないため,処理にかかる資源はFilterによる絞り込みが有効な状態と変わりありません。

E 例

[KEY]というフィールドを含むレコードセットに対してFilterを適用し,[KEY]の値を更新してUpdateBatchを実行する例を示します。
レコードセットが接続状態レコードセットであるか,非接続レコードセットであるかには影響されません。

サンプルコード
(レコードセット変数としてrsSQLが適切に確保されているとします)

rsSQL.Filter = "[KEY] LIKE 'A%'"		・・・@
rsSQL.MoveFirst				・・・A

Do rsSQL.EOF
    rsSQL.Fields("KEY") = _
                  Replace(RsSQL.Fields("KEY"),"A","B")		・・・B
    rsSQL.Fields(***) = ***			・・・C
	・・・
    rsSQL.MoveNext
Loop

'rsSQL.UpdateBatch				・・・D
rsSQL.Filter = adFilterNone			・・・E
rsSQL.UpdateBatch				・・・F
								

@・・・ Filterを設定してKEYの先頭がAのレコードに絞り込みます。
A・・・ 安全のためカーソルをうつします。
B・・・ KEYの先頭AをBに置き換えます。ここでレコードは絞り込みから外れるレコードになります。
C・・・ 他のフィールドを更新します(必要に応じて)。
D・・・ ここでの更新は適切に送信されません。
E・・・ Filterを解除します。
F・・・ この更新が正常に送信されます。

2 UpdateBatchの挙動

同時実行制御としてのUpdateBatch

UpdateBatchメソッドによる更新処理はCancelBatchメソッド及びResyncメソッドとあわせて利用することによって,競合に対応する同時実行制御の一形態として効果的に利用することができます。
同時実行制御としてのUpdateBatch処理の特長は次のようになります。

@ 更新の競合が起こっていない,書き込み可能な更新は全て書き込む
競合が発生していない更新については更新をプールしておいて一括で書き込んでいるという点以外は,随時更新処理と本質的な違いはありません。
(非接続レコードセットを使った場合の接続資源の有効利用は除きます)


A 競合が発生した更新のみに特化して,現在保持している更新をどうするか制御することができる
UpdateBatchは競合を起こした更新を含むレコードのステータスに失敗をマーキングし,エラーを起こします。
ここで同時実行制御に失敗したエラー(-2147217864)をトラップすることによって,失敗した処理に対する対応を吟味するチャンスが生まれます。
プログラマーは
  • 1.CancelBatchメソッドを呼び出して競合した更新を一括で破棄すること,
  • 2.Resyncメソッドを介して全ての更新を上書きすること,
  • 3.競合した更新の内容同士を詳細に比較して,どちらの更新を採用するかを評価すること
の3つの選択肢を得ることができます。
UpdateBatchが競合のために処理を停止する単位はレコードですが,競合を起こしたレコード内の各フィールド毎にOriginalValue,Value,UnderlyingValueの各プロパティを評価することによって,更新を残すフィールドの取捨選択が可能です。
よって,UpdateBatchは最終的にフィールド単位の同時実行制御を行うことが可能です。
(制御方法の詳細については「ADOの最適化」を参照のこと。)

標準のメソッドを利用することによってこれらの制御を実装できること。
また,これらの制御を一連の更新に対して一括で制御できること。
以上の2点から,同時実行制御が必要なコーディングの際にUpdateBatch処理は,随時更新に比較して非常に有用です。

同時実行制御としてのトランザクション処理との違い

UpdateBatchは同時実行制御の一形態を示しますが,トランザクション処理とは違いがあります。
トランザクションは問題発生時に,一連の更新を全てロールバックすることによって,処理全体に対して0か1かの処理を行うことができます
一方,UpdateBatchは問題が発生したデータのロールバックはできますが,正常更新したデータを元に戻すことは難しくなります
よって,更新の一貫性をどのように確保するかの仕様によって以下の様に分類することができます。

@・・・ 問題の発生状況に応じて処理一連を書き込むか書き込まないかの処理を行う要請ではトランザクションを利用する
データベース操作に原子性が要請される場合はトランザクションを採用することがもっとも自然です。
更新が小規模で,かつトランザクション処理によるデッドロックの発生が許されない場合は,UpdateBatchメソッドの機能とOriginalValueプロパティを組み合わせることによって全データを手動でロールバックすることは可能ですが,複雑な評価アルゴリズムが必要になります。

A・・・ レコード単位またはフィールド単位で競合しない更新は随時確定を許すような要請ではUpdateBatchを利用する
0か1かの処理がトランザクションの専門領域であるのに対して,こちらの要請はUpdateBatchの専門領域になります。
上述のとおり,UpdateBatchメソッドが標準で競合を制御する単位はレコードですが,競合レコード内の値プロパティ群を評価することによってフィールドごとのより細やかな制御を行うことも可能です。

B・・・ 事前保持した更新予定情報の全てが書き込まれるまで再試行するトランザクションはUpdateBatchをショートサーキットとして利用できる場合がある
事前に取得した更新予定情報が全て確定されるまでトランザクションを再試行するような要請では,データの一貫性を確保する制御をトランザクション以外の処理に切り離すことを条件に,UpdateBatchによって処理が効率化できる場合があります。
例えば,トランザクションの説明によく利用される銀行振込の例を用いると,
「Aの口座からBの口座へ10,000円振り込む」という処理は通常トランザクションとして扱われます。
これは
  • 1. Aの口座から10,000円差し引く
  • 2. Bの口座に10,000円加える
という2つの処理を1単位として実行することが原子性の面から必要となるからです。

しかし,見方を変えてみると,「Aの口座からBの口座へ10,000円振り込む」という処理がユーザーに確定されていて,アプリケーションがこの確定事項の情報を保持している場合最終的に1と2の処理がデータベースに保存されていることが最終的な必要要件となります。
これはトランザクションが正常終了するまで再試行を続けることで実装できますが,「確定できる情報は確定してしまう」UpdateBatchの処理でも処理することが可能です。
この場合,原子性を確保するために全ての変更が確定されるまで,Resyncを介した更新の再送信を続けることになります。

このときのトランザクションとUpdateBatchのデータベース資源の利用状況を比較すると,
  • トランザクション処理:全ての更新が正常に確定されるまで全更新をロールバック+再試行するため,送信データ数は再送信ごとに常に一定で最大数
    また,一連のトランザクション処理が終了するまでロックを取得するため,ロック競合を起こす頻度が非常に高い。
  • UpdateBatch:確定情報を再送信しないので,送信データ数は再送信ごとに減少していく
    また,ロックレベルがトランザクションより低いためにロック競合の頻度は比較的低い。
となるために,この場合のUpdateBatchはトランザクション処理のショートサーキットとして機能することになります。

ただし,これは例の中の銀行口座データベースが「残高のみを管理する」及び「マイナスの残高をゆるす」という仕様になっていることが必要になります
これは「AからBへ振り込み」「Aから引き落とし」という処理が続けて起こった場合,
  • 残高のみの管理:ある時点で全ての金額の更新が確定されていればよい
  • 出納通帳を含めた管理:一連の引き落とし順も含めて管理する必要がある
という要請の違いによって,処理の確定順にも一貫性を持たせる必要の有無が別れることによります。
UpdateBatchの処理では確定順序の一貫性の確保は難しくなります。

3 トランザクション処理に関する基礎

トランザクション処理は複数のデータ更新を1つの実行単位としてまとめ,全てを処理するか,全ての処理を破棄するかを制御することができるため,複数のテーブルに対するデータ更新の整合性を確保するために有効です
しかし,トランザクション処理は通常のデータ更新を複数含んで1つの実行単位とするために,必然的に処理時間が長くなります
また,データの整合性を確保する要請から,データに対する重大なロックを取得するのが通常です

このことによりトランザクション処理は長時間にわたる重大なロックを取得して,他のプロセスがデータベース資源にアクセスすることを阻害してしまいます
この同時実行性を制御するために,トランザクション分離レベルやロック粒度を調整することも有効ですが,一般的には下記のガイドラインに沿ってトランザクションをできるだけ短時間に抑えることが必要です。

トランザクション処理の単位を可能な限り小さくする

処理の設計の段階でトランザクション処理に含める論理的な処理を吟味します。
一貫性を確保する処理単位を最小規模に分離します。
処理を最小単位に分割しておかないと,データベース資源の無駄遣いが発生すると同時にデータの矛盾を防止できません。
論理単位よりもトランザクション処理が小さい場合,ロールバックするべき場合に戻すべきデータが戻らずにデータの整合性が失われます。
論理単位よりも大きい場合は戻さなくてもよい情報までも戻すことになり,無駄な処理を実行することになります。
一連の処理の中に,データの確定を行うことができるコミットポイントが存在する場合は,処理を2つのトランザクションに分割します。

トランザクション内でアクセスするデータ量を可能な限り少なくする
トランザクション内でアクセスされたデータベース資源にはロックが掛けられます。
ロックの衝突頻度を最小限に抑えるために必要最低限の資源にアクセスするようにSQL文を加工します。
ロックとは別に処理速度の問題から,トランザクション内の更新処理には極力レコードセットを利用しないことが推奨されます。
SQLコマンドを発行してデータを更新する操作に比べて,レコードセットを介した更新ではカーソル操作に関わるオーバーヘッドが発生します。
また,レコードセットを使用する場合でも,カーソル移動の負荷を軽減するためにも更新に必要な最低限の規模のレコードセットを取得するべきです。

トランザクション処理に含めるデータ操作はSQLステートメントのINSERT,UPDATE及びDELETEに相当する操作のみに限る
トランザクションはデータ更新の整合性を確保するための機能であるため,トランザクション内に含まれるデータ操作ステートメントはINSERT,UPDATE及びDELETE等のデータ操作文が主体になります。
TRUNCATE,BULK INSERTなどの大規模操作文や,インデックスの再計算,データ定義の変換などの長時間にわたる処理などは含めるべきでありません。
レコードセットを介したトランザクションの場合も,データスキーマに関わる操作は排除します。

ユーザー応答を必要とする処理はトランザクション処理前に全て完了させる
トランザクション中にユーザー応答の待機を行ってしまうと,ユーザー応答が完了されて処理が再開されるまでの応答待ち時間までロック時間に加わってしまいます。
これはデータ処理にかかる最低限のロック時間に比して致命的に長い時間となってしまいます。
トランザクション処理では,リアルタイムなユーザー応答を実装せずに,論理単位内で必要な入力情報は事前に取得してプールする構造が必要になります。

データの参照及び解析はトランザクション処理前に全て完了させる
不可能な場合がほとんどですが,データの参照中にトランザクションを開始したり,トランザクション処理中に新たにデータの参照を行うといった処理は推奨されません
データの参照及び解析が必要な場合は,トランザクション開始前に全て処理するように設計します
事前に参照・解析を済ませることによって,参照・解析にかかる処理時間をトランザクション処理時間(ロック保持時間)から分離することができます。

4 SQL Serverでのロック

Enterprise Managerでのロックの見方

Enterprise Managerでは"管理>現在の利用状況>ロック/プロセスID"画面でプロセスごとのロック取得状況を確認できます。
但し,リアルタイムに更新される情報ではないので,必要な時点ごとにリフレッシュする必要があります
この画面ではロック競合によってブロックしたプロセスとブロックされたプロセスを確認することができます。
ブロックをした原因プロセスには「!」が,解放待ちをしているプロセスには「■」が表示されます

図4-1 ロックブロッキングの表示

一方,デッドロックはSQL Serverが自動で処理をしてしまうために,管理画面で補足するのは非常に難しくなります。
デッドロックの評価には付属ツールのSQL Serverプロファイラを利用して,テンプレートに用意されているデッドロックの原因の識別を利用します。
イベントクラスに表示されるLock:Deadlock及びLock:Deadlock Chainの情報がデッドロックに関わる情報です。

図 4-2 デッドロックの原因の識別

インテントロック
SQL Serverは競合を最小化するために,メインに必要なロックの他にインテントロックを設定します。
インテントロックは,現在アクティブな自プロセスが取得しているロックを含むようなより包括的で広範囲なロックを他のプロセスに取得されることを防ぐためのロックです。
よってインテントロックは実体的なデータリソースをロックするものではありません
例えば,プロセス1がTABLE1の特定行の排他ロックを取得しているとします。
この場合,プロセス2がTABLE1全体の排他ロックを取得することを防止するために,プロセス1はTABLE1についてインテント排他ロックを取得します。
この時,プロセス2はプロセス1が保持している特定行を含まない行レベルの排他ロック及びTABLE1のインテント排他ロックを取得することができますが,TABLE1全体をロックする,実体的な排他ロックを取得することはできません。

ロック取得の相互互換性

ロック取得の相互互換性は下記の互換性マトリックスに従います。
マトリックスに従って,行,インデックス,ページ,エクステント,テーブル全体,データベース全体の各階層ごとに互換性を判断することになります。

互換性 既存ロック
要求ロック IS S U IX SIX X
インテント共有(IS) ×
共有(S) × × ×
更新(U) × × × ×
インテント排他(IX) × × ○※ × ×
インテント排他付共有(SIX) × × × × ×
排他(X) × × × × × ×

※ インテント排他ロックは一部の行を更新するために取得されるため修正行がかぶらない他のインテントロックと共存できます。

5 ConnectionオブジェクトのExecuteメソッドのみによるトランザクション処理

最も基本的なトランザクション処理の形態です。
本質的にSQL ステートメント"BIGIN TRUNSACTION"を利用してトランザクションを制御する構成と変わりありません。
アプリケーション側で事前に必要な情報を全て蓄積して,構成した更新情報をトランザクションとして送信します。

必要な事前情報は,ユーザー応答や環境変数などを含むアプリケーション内部の状態及び現在のデータベース内のデータなどから取得することになります。
この際,データベースから取得する情報は読み取り専用のレコードセットとして取得します

また,この読み取り専用レコードセット用のコネクションはトランザクションを実行するためのコネクションとは別に取得して分離しておくことによって,コネクションの解放手順を分離することができます。
但し,読み取り用の共有ロックが存在している状態でトランザクション用の排他ロックを取得することはできないので,読み取り専用のレコードセットによるロックはトランザクション処理開始前に解放しておくことが推奨されます。
また,任意のSQLコマンドを任意のテーブルに対して送信することができるため,比較的高速で安定的に複数テーブル間の整合性確保を含んだトランザクションを実行することができます。

コード例
On Error Goto Catch

    Dim cnTXN As ADODB.Connection			・・・ @
    Dim strSQLQueue() As String			・・・ A
    Dim intI As Integer
    Redim strSQLQueue(10)

strSQLQueue(1) = "UPDATE [TST_ADO] SET [Number] = [Number] + 1;"
strSQLQueue(2) = "UPDATE [TST_ADO2] SET [Number] = [Number] + 1;"

	・・・    					・・・ B

    Set cnTXN = New ADODB.Connection			・・・ C
    cnTXN.Open "Provider=SQLOLEDB.1;Data Source=INTSTSRV01;" & _
        " Trusted_Connection=Yes;Initial Catalog=TST_ADO"

    cnTXN.BeginTrans				・・・ D

For intI = 1 To 10					・・・ E
            cnTXN.Execute strSQLQueue(intI), , adCmdText + adExecuteNoRecords
    Next

cnTXN.CommitTrans					・・・ F

Catch:
    If Not(cnTXN Is Nothing) Then			・・・ G
       If cnTXN.State <> adStateClosed Then cnTXN.RollbackTrans
    End If
				

@・・・ トランザクション用のコネクションオブジェクトを宣言します
トランザクションに関わるプロセス情報を一本化し,既存の他の接続と分離するため,トランザクション用のコネクションを専用に宣言します。
専用コネクションはトランザクション処理を行う間だけ有効になるように,可能な限りローカルな変数として宣言します。


A・・・ トランザクション処理で送信するSQL文を保存しておく変数を宣言します
トランザクションで送信する更新をSQL文の形に整形して保存しておくための一時領域です。


B・・・ 送信するSQL文を生成します
トランザクションで送信する更新に関わる情報をここで全て取得します。
この処理の間では各種のレコードセットを生成して更新に利用するデータを取得すること及びデータを解析することを行えます。
取得した情報を元に,実際の更新を行うSQL文を生成して,一時領域にプールします。


C・・・ トランザクション用コネクションを開きます
トランザクション処理用のコネクションはトランザクション開始の直前に開きます。
このコネクションが有効化されている間は,トランザクション実行中とアプリケーション側では判断します。
(厳密にはリアルタイムで一致しているわけではありません)


D・・・ トランザクションを開始します
トランザクション開始を明示します。開始後はデータの更新のみを可能な限り迅速に行います。
トランザクションの開始に関わるエラーは以下の2つになります(サーバーとの接続状況など,コネクションを原因とするエラーは除きます)。
どちらもエラーが発生した時点でSQL Serverは内部の更新処理を全てロールバックします。
プログラマーは利用しているADO内部のトランザクション実行状態をロールバックするためにRollbackTransメソッドを呼び出します。
(SQL Serverはネットワーク不良,アプリケーションの強制終了などの障害でトランザクションのプロセスが切断された場合にも適切にロールバックを実行します)
  • 1.ロック取得待ちのタイムアウトエラー (-2147217871)
    他のプロセスの既得ロックに阻害されて自プロセスがロックを取得できないためにトランザクションが開始できない場合に発生します。


  • 2.デッドロック発生のエラー(-2147467259)
    エラー番号はSQL Server用のプロバイダーが共通で返す一般的なエラーの番号ですが,コネクションオブジェクトのErrors.NativeErrorプロパティが返す内部エラー番号が1205になります。
    2つのトランザクションが同時実行された場合に発生し,通常はデッドロックチェインとなります。
    デッドロックの問題解決自体はSQL Serverが自動で行うため,プログラマーは通常の更新失敗とみなして必要な処理を続行することが可能です。


E・・・ 保存されている更新を送信します
プールされている更新を全て送信します。


F・・・ トランザクションをコミットします
全ての更新を送信した時点でトランザクションのコミットを行います。
特にトランザクションのコミットに他の条件が関わる場合は,ここで評価を行い,変更を確定するか,戻すかを判断します。


G・・・ 何らかのエラーが発生した場合はトランザクションをロールバックします
SQL Serverが解決するロックなどの問題が発生した時点でSQL Server内部のトランザクションはロールバックされています。
プログラマーはADOに対してトランザクション処理の取り消しを指示するためにRollbackTransメソッドを呼び出します。
また,通常は行うべきではありませんが,トランザクション実行中にSQL Server以外の要因でランタイムエラーが発生するような処理を行っている場合には,このRollbackTransメソッドによってSQL Server及びADOにトランザクションのロールバックが指示されます。
※ ADO資源の解放に関わる処理は省略してあります。

コマンド送信の最適化

この例では,全てのSQL ステートメントをcnTXN.Executeによって送信しています。
この方法は文法エラー等の個々のSQLステートメントのがエラーを起こした場合に原因ステートメントを見分ける際には利点があります
しかし,Executeメソッドの実行を繰り返すことによって送信ごとにADO利用によるオーバーヘッドが繰り返されるために,ユーザー応答時間が不安定になりがちで,かつ処理に無駄な時間がかかります。

一方下記の様にSQL文を行区切記号";"(セミコロン)で区切ったバッチステートメントとして整形して一括の更新としてExecuteメソッドに渡せば,Executeメソッドの連鎖呼び出しによるオーバーヘッドを防止できるために非常に高速になります
ただし,この方法の場合はどの更新ステートメントがエラーを起こしたかを特定することが非常に難しくなります。

サンプルコード
strSQLQueue(1) = "UPDATE [TST_ADO] SET [Number] = [Number] + 1;"
strSQLQueue(2) = "UPDATE [TST_ADO2] SET [Number] = [Number] + 1;"

	・・・    					

For intI = 1 To 10
    strSQLTemp = strSQLTemp & strSQLQueue(intI)
Next			・・・ @

Set cnTXN = New ADODB.Connection

cnTXN.Open "Provider=SQLOLEDB.1;Data Source=INTSTSRV01;" & _
    " Trusted_Connection=Yes;Initial Catalog=TST_ADO"

cnTXN.BeginTrans
    cnTXN.Execute strSQLTemp, , adCmdText + adExecuteNoRecords
cnTXN.CommitTrans		・・・ A
						

@・・・ 区切り文字を含んだSQL文を連結してバッチステートメントを生成します。
A・・・ 生成したバッチステートメントを送信します。

6 Executeメソッドと非接続レコードセットの組み合わせによるトランザクション処理

主要な更新を行うレコードセットを取得して,他テーブルとの整合性を確保する処理のみをExecuteメソッドで実装する形態です。
メインのレコードセットに対してリアルタイムなユーザー応答によって更新をセットする場合に効果的です。
ただし,この場合もトランザクション処理を最短化するために非接続レコードセットによるバッチ更新処理を行う必要があります。 また,アプリケーションの設計の段階で,レコードセットを取得するプロシージャ,レコードセットに変更を加える機能,レコードセットの更新をトランザクション更新するプロシージャをそれぞれ完全に分離しておくことが推奨されます。
レコードセットの変更とトランザクション更新を同じプロシージャに含める場合は,トランザクションを開始する前に全ての更新をレコードセットに書き込んでおきます。

サンプルコード
Private Sub SubmitChange()
On Error Goto Catch
    Dim cnTXN As ADODB.Connection
    Dim strSQLQueue() As String
    Dim intI As Integer

    Redim strSQLQueue(10)
    strSQLQueue(1) = "UPDATE [TST_ADO] SET [Number] = [Number] + 1;"

	・・・    							・・・ @

    Set cnTXN = New ADODB.Connection			・・・ A
    cnTXN.Open "Provider=SQLOLEDB.1;Data Source=INTSTSRV01;" & _
        " Trusted_Connection=Yes;Initial Catalog=TST_ADO"

    Set rsMain.ActiveConnection = cnTXN ・・・ B

    cnTXN.BeginTrans			・・・ C
        rsMain.UpdateBatch		・・・ D
        For intI = 1 To 10
            cnTXN.Execute strSQLQueue(intI), , adCmdText + adExecuteNoRecords
    Next
    cnTXN.CommitTrans			・・・ E

Catch:
    If Not(cnTXN Is Nothing) Then	・・・ F
        If cnTXN.State <> adStateClosed Then cnTXN.RollbackTrans
    End If
				

@・・・ 整合性確保のために,メインのレコードセットがアクセスする以外のテーブルに対する更新をセットします。


A・・・ 共有するトランザクション用コネクションを確保します


B・・・ 共有コネクションを非接続レコードセットの再接続に使用します。


C・・・ トランザクションを開始します。


D・・・ 更新を全て送信します。
レコードセットに対するUpdateBatchとExecuteによる更新を送信します。
更新処理定義の必要要件に従えば,レコードセットに対する更新とExecuteによる更新の順番は任意です。


E・・・ トランザクションをコミットします。


F・・・ エラー発生時にはロールバックを実行します。
非接続レコードセットを使用しているため,更新の競合に関するエラーは高い頻度で発生する可能性があります。
この場合にもいち早くRollbackTransメソッドによってトランザクションを終了して,ロックを解放する必要があります
その後,非接続レコードセットの競合処理機能を利用して競合の状態を評価して,再更新を試みる場合は新たな別のトランザクションとして再送信することが推奨されます。
UpdateBatchのエラーにより一度停止したトランザクションを保持したまま状態評価を含む制御を行うと,ロック保持時間が伸びるため,他プロセスと連鎖的なエラーを引き起こす原因になります。

7 コネクション共有による2つの非接続レコードセットのトランザクション処理

更新を行うレコードセットが複数ある場合に実装する形態です。
全てのレコードセットでトランザクション用のコネクションを共有すれば,複数テーブル間の整合性を確保することができます。
分散したレコードセットに対して,トランザクション実行時に共有の接続を設定するために,全てのレコードセットを非接続レコードセットにしておくことが推奨されます。
ただし,複数のレコードセットに対して同時にUpdateBatchメソッドを連続実行することになるため,バッチ更新の競合を評価するアルゴリズムが複雑になります。

サンプルコード
Private Sub SubmitChange()
On Error Goto Catch
    Dim cnTXN As ADODB.Connection

    Set cnTXN = New ADODB.Connection	・・・ @
    cnTXN.Open "Provider=SQLOLEDB.1;Data Source=INTSTSRV01;" & _
        " Trusted_Connection=Yes;Initial Catalog=TST_ADO"

    Set rsMain.ActiveConnection = cnTXN		・・・ A
    Set rsSub.ActiveConnection = cnTXN

    cnTXN.BeginTrans			・・・ B
        rsMain.UpdateBatch		・・・ C
        rsSub.UpdateBatch
cnTXN.CommitTrans			・・・ D

Catch:
    If Not(cnTXN Is Nothing) Then	・・・ E
        If cnTXN.State <> adStateClosed Then cnTXN.RollbackTrans
    End If
				

@・・・ 共有するトランザクション用コネクションを確保します


A・・・ 共有コネクションを全ての非接続レコードセットの再接続に使用します。


B・・・ トランザクションを開始します。


D・・・ 更新を全て送信します。
レコードセットに対するUpdateBatchを全レコードセットに対して適用します。
この際,バッチ更新競合を起こしたレコードセットがどのレコードセットなのかを判定する処理が必要になります。
UpdateBatchを適用する間のみ,On Error Resume Nextを利用したインラインエラー処理を利用することも一つの手法です。


E・・・ トランザクションをコミットします。


F・・・ エラー発生時にはロールバックを実行します。
複数のレコードセットにUpdateBatchを適用するために,エラーにより一度停止したトランザクションを保持したまま状態評価を含む制御を行うと,ロック保持間が伸びるため,他プロセスと連鎖的なエラーを引き起こす原因になります。
トランザクションを停止した後に,各レコードセットのエラー状態を評価して,レコードセットごとに適切な処理を実行する必要があります。

8 ADOトランザクションで不可能なこと

トランザクション処理による排他性の高いロックのために,複数のレコード参照を実行するような処理は実行できません。
トランザクションの最短化のためにも,トランザクション処理内では更新の送信のみを行う必要があります。

サンプルコード
Dim cnSQL As ADODB.Connection
Dim rsSQL As ADODB.Recordset
Dim rsSQL2 As ADODB.Recordset

Set cnSQL = New ADODB.Connection
Set rsSQL = New ADODB.Recordset
Set rsSQL2 = New ADODB.Recordset

cnSQL.Open "Provider=SQLOLEDB.1;Data Source=IXWRKSRV10;" & _
        "Trusted_Connection=Yes;Initial Catalog=TST_CHOKAN"

rsSQL.Open "SELECT * FROM [TST_ADO]", cnSQL, adOpenKeyset, _
adLockOptimistic, adCmdText
cnSQL.BeginTrans
Set rsSQL = cnSQL.Execute("SELECT * FROM [TST_ADO2]", , adCmdText)
Set rsSQL2 = cnSQL.Execute("SELECT * FROM [TST_ADO2]", , adCmdText) ・・・ @
				

@・・・ 2つ目の読み取り専用レコードセットを取得する時点で,-2147467259:手動または分散トランザクションモードのため,新規接続を開始できませんというエラーが発生します。