DB 設計者のための明解 ADO.NET 第 1 回

マイクロソフト株式会社
デベロッパー・マーケティング本部
.NET マーケティング部
デベロッパー エバンジェリスト  アーキテクト エバンジェリスト
伊藤英豪

公開日 : 2003 年 3 月 14 日
(改訂 : 2003 年 3 月 12 日; 初出 : 「SQL Server マガジン 2003 Vol.5」 翔泳社発行)

連載開始にあたって

ADO.NET の基本的な機能に関しては、すでにさまざまな媒体を通じてご存知の方も多いと思われる。ADO.NET は ADO という呼称を冠しているが、実のところ ADO の単なる .NET 対応 (マネージドコード化) といったレベルのものではない。

ADO を始め、従来のデータアクセスは、主に DBMS との接続を前提とした、いわゆる 2 階層モデルに基づいている。しかし、近年の Web アプリケーションや XML Web サービスといったデータ処理では、多階層アーキテクチャが多用されている。そのため、接続をベースにした従来のアプローチから、非接続をベースにした新しいアプローチへと切り替えることによって、より優れたスケーラビリティを確保する必要に迫られている。

ADO.NET が出てきた目的は、まさにこの次世代のアプリケーション構築にふさわしいデータアクセスアーキテクチャの提供にあると言える。しかしながら、この非接続データアクセスと楽観的同時実行制御に関して、正しく理解されていないことも多い。今回は、ADO.NET でのデータアクセスに関して、従来のような接続型と新たに登場した非接続型のメカニズムの共通点と相違点を解説する。これらのデータアクセスアーキテクチャについての理解を深め、それぞれの技術を適用する際の判断材料としていただきたい。

なお、本記事中のサンプルは SQL Server 7.0 以降対応の SQL Server .NET マネージドプロバイダの使用を前提としているが、基本的な考え方に関しては、ほかの OLEDB データプロバイダを使用した場合でも同様である。

接続型データアクセス

接続型データアクセスとは、DBMS との接続を保ったまま一連の処理を実行していくものである。たとえば、Web Form へのデータ表示に関しても、DBMS と接続したまま各項目値をコントロールに転記していき、処理終了時に明示的に接続を解放する。この処理形態の特徴は、一連の処理の実行中、接続が常に維持されていることである。

次のリスト 1 は、典型的な接続型データアクセスによる ADO.NET のコードである。これは VB .NET のコードだが、C# でも同様のコードとなる (違いは言語の構文レベルのみ)。

リスト1 接続型データアクセスのサンプル

Sub Example1_1()
  ' 接続型データアクセスのサンプル
  Dim sqlDataRdr As SqlDataReader
  Dim sqlConn As SqlConnection
  Dim sqlCmd As SqlCommand
  Try
    'データベースへの接続文字列を引数にSqlConnection オブジェクトを生成
    sqlConn = New SqlConnection(getConnectionString)
    ' SqlCommand オブジェクトを生成
    sqlCmd = New SqlCommand()
    'Command オブジェクトのプロパティにSQL 文を設定
    With sqlCmd
    .CommandType = CommandType.Text
    .CommandText = "SELECT * FROM Products"
    .Connection = sqlConn
  End With
  ' データベースコネクションを開く
  sqlConn.Open()
  ' SQL 文を発行し、検索結果をSqlDataReader オブジェクトとして受け取る
  sqlDataRdr = sqlCmd.ExecuteReader()
  While (sqlDataRdr.Read) ' 各行に対して処理を繰り返し
    Diagnostics.Debug.WriteLine(sqlDataRdr.Item("ProductID").ToString() & "," & _
      sqlDataRdr.Item("ProductName").ToString())
    End While
  Catch E As Exception
    Diagnostics.Debug.WriteLine(E.ToString)
  Finally
    If Not (sqlDataRdr Is Nothing) Then
    sqlDataRdr.Close()
    End If
    If Not (sqlConn.State = ConnectionState.Closed) Then
      sqlConn.Close() 'コネクションの解放
    End If
  End Try
End Sub

コードの基本的な流れは、DB へのコネクションを生成し、SQL 文を発行し、結果を DataReader オブジェクトで受け取り、各行に対して処理をする、といった具合になっている。データアクセスの方法は、ADO と似た一般的な方法であると言える (参考までに、同様のデータアクセスをストアドプロシージャで実行した場合のコードを次のリスト 2 に掲載しておく)。コードを見るとわかるように、Select 文を実行して複数の検索結果を処理する場合には、Command オブジェクトの ExecuteReader() メソッドを実行し、前方スクロールのみの読み取り専用の DataReader オブジェクトを使用したコードとなる。

リスト 2 接続型データアクセスのサンプル (ストアドプロシージャの場合)

Sub Example1_2()
  ' 接続型データアクセスのサンプルストアドプロシージャの使用
  Dim sqlDataRdr As SqlDataReader
  Dim sqlConn As SqlConnection
  Dim sqlCmd As SqlCommand
  Dim param As SqlParameter
  Try
    sqlConn = New SqlConnection(getConnectionString)
    sqlCmd = New SqlCommand()
    With sqlCmd
      .CommandType = CommandType.StoredProcedure
      .CommandText = "GetProducts"
      .Connection = sqlConn
    End With
    param = sqlCmd.Parameters.Add(New SqlParameter("@CategoryID",
  SqlDbType.Int))
    param.Direction = ParameterDirection.Input
    param.Value = 1
    sqlConn.Open()
    sqlDataRdr = sqlCmd.ExecuteReader()
    While (sqlDataRdr.Read)
    Diagnostics.Debug.WriteLine(sqlDataRdr.Item("ProductID").ToString() & "," & _
      sqlDataRdr.Item("ProductName").ToString())
    End While
  Catch E As Exception
    Diagnostics.Debug.WriteLine(E.ToString)
  Finally
    If Not (sqlDataRdr Is Nothing) Then
      sqlDataRdr.Close()
    End If
    If Not (sqlConn.State = ConnectionState.Closed) Then
      sqlConn.Close()
    End If
  End Try
End Sub

これに対し、更新系の処理を行う基本的なコードサンプルは、次のリスト 3 のようになる。参照系の処理では、結果を取得するために ExecuteReader() メソッドを使っていたが、更新系の処理ではデータを取得するわけではないので、ExecuteNonQuery() メソッドを使用して SQL 文やストアドプロシージャを起動する。なお、この際の返却値は処理の対象となったレコード件数となる。処理対象件数が 0 件であることをエラーとする必要があれば、何らかの対応の実装が必要となる。

リスト3 接続型データアクセスの更新系サンプル

Sub Example1_3()
  ' 接続型データアクセスの更新系サンプル
  Dim sqlConn As SqlConnection
  Dim sqlCmd As SqlCommand
  Dim rowsAffected As Integer
  Try
    ' Connection オブジェクトの生成
    sqlConn = New SqlConnection(getConnectionString)
    ' Command オブジェクトの生成
    sqlCmd = New SqlCommand()
    ' Command オブジェクトに対して、SQL 文、コネクションオブジェクトを設定
    With sqlCmd
      .CommandType = CommandType.Text
      .CommandText = "UPDATE Products SET ProductName = 'Chai_1_3' WHERE ProductID=1"
      .Connection = sqlConn
    End With
    ' データベースコネクションを開く
    sqlConn.Open()
    ' 更新系SQL を実行し、更新した件数を返却値として取得
    rowsAffected = sqlCmd.ExecuteNonQuery()
    ' 更新件数を見て何らかの処理をここに記述
    Diagnostics.Debug.WriteLine(rowsAffected.ToString & " row(s) affected.")
  Catch e As Exception
    Diagnostics.Debug.WriteLine(e.ToString)
  Finally
    If Not (sqlConn.State = ConnectionState.Closed) Then
      sqlConn.Close()
    End If
  End Try
End Sub

このように接続型データアクセスの基本的なコードに関しては、従来の ADO とほぼ同じである。重要なのは、データアクセスの実行中、データベースコネクションが必ず開いていなければならない点である。また、使い終わった接続は明示的に解放しておかないと、ほかからの再利用ができない。

接続型データアクセスでのトランザクション制御は、従来の ADO とほぼ同様だが、Connection オブジェクトの BeginTransaction メソッドで Transaction オブジェクトを生成し、Command オブジェクトの Transaction プロパティに Transaction オブジェクトをセットすることでトランザクション処理の開始を宣言する (このときに、トランザクションの分離レベルも指定できる)。

リスト 4 は、接続型データアクセスのトランザクション制御のサンプルである。

リスト4 接続型データアクセスのトランザクション更新のサンプル

Sub Example1_5()
  ' 接続型データアクセスのトランザクション更新のサンプル
  Dim sqlConn As SqlConnection
  Dim sqlCmd As SqlCommand
  Dim sqlTrans As SqlTransaction
  Dim rowsAffected As Integer
  Dim sqlDataRdr As SqlDataReader
  Try
    sqlConn = New SqlConnection(getConnectionString)
    sqlCmd = New SqlCommand()
    sqlConn.Open()
    sqlTrans = sqlConn.BeginTransaction(IsolationLevel.Serializable)
    With sqlCmd
      .CommandType = CommandType.Text
      .Connection = sqlConn
      .Transaction = sqlTrans
    End With
    
    '以下のコメントアウトは、更新対象に対しあらかじめ更新ロックをかける場合のコード
    'sqlCmd.CommandText = "SELECT * FROM Products WITH(UPDLOCK) WHERE ProductID=1"
    'sqlDataRdr = sqlCmd.ExecuteReader()
    'While (sqlDataRdr.Read) 'レコードのRead によって更新ロックがかかる
      ' Diagnostics.Debug.WriteLine(sqlDataRdr.Item("ProductID").ToString() & "," & _
        ' sqlDataRdr.Item("ProductName").ToString())
    'End While
    'sqlDataRdr.Close() 
    'DataReader はいったん閉じておかないと未クローズの実行時エラーとなる
    
    sqlCmd.CommandText = "UPDATE Products SET ProductName = 'Chai_a' WHERE ProductID=1"
    rowsAffected = sqlCmd.ExecuteNonQuery()
    sqlCmd.CommandText = "Insert Products (ProductName) Values ('chai_1_5')"
    rowsAffected = sqlCmd.ExecuteNonQuery()
    Diagnostics.Debug.WriteLine(rowsAffected.ToString & " row(s) affected.")
    sqlTrans.Commit()
    Diagnostics.Debug.WriteLine("Both records are written to database.")
  Catch e As Exception
    Diagnostics.Debug.WriteLine(e.ToString)
  Finally
    If Not sqlTrans.Connection Is Nothing Then
      sqlTrans.Rollback("SampleTransaction")
    End If
    If Not (sqlConn.State = ConnectionState.Closed) Then
      sqlConn.Close()
    End If
  End Try
End Sub

コラム : ADO.NET でのサーバーカーソル操作

ADO.NET では、サーバーカーソルがサポートされていない。システムのスケーラビリティ低下の原因になるからである。ただし、Command オブジェクトの CommandText プロパティに Transact-SQL ステートメントによるカーソル操作を埋め込んでおき、ExecuteNonQuery() メソッドによってサーバーカーソル操作を実現することも可能である。しかし、このような方法はむしろストアドプロシージャによって実現する方が保守性が上がる。また、カーソル操作の多用はスケーラビリティ低下の要因ともなりえるので注意が必要である。

非接続型データアクセスの動作とそのアドバンテージ

非接続データアクセスは、ADO.NET の中心的なオブジェクトである DataSet によってもたらされる。「非接続」 という表現の意味するところは、DBMS との接続を常時維持し続けることなくデータベースのデータを操作できる、ということである。非接続環境では、インメモリの DataSet オブジェクトにデータベースのデータがコピーされた後、いったんデータベースとの接続が断たれる。次にデータへのアクセスが発生したときには、このローカルのコピーに対してアクセスが行われる。更新されたデータをデータベースへ反映するときには、接続が一時的に復元される。リスト 5 に非接続型の基本的なコード例を示したので、これを基に非接続のデータアクセスの実際を具体的に見ていこう。

リスト 5 非接続データアクセスでの複数件の検索

Sub Example2_1()
  ' 非接続データアクセスでの複数件の検索
  Dim sqlDA As SqlDataAdapter
  Dim resultDS As DataSet
  Dim i As Integer
  ' DataAdapter オブジェクトの生成
  sqlDA = New SqlDataAdapter()
  ' DataSet オブジェクトの生成
  resultDS = New DataSet()
  With sqlDA
      ' DataAdapter にSelectCommand を追加
      .SelectCommand = New SqlCommand()
      ' SelectCommand の各種プロパティを設定
    With .SelectCommand
      .CommandType = CommandType.Text
      .CommandText = "SELECT ProductID, ProductName FROM Products"
      .Connection = New SqlConnection(getConnectionString)
    End With
  End With
  ' 検索結果をDataSet へ格納
  Try
    sqlDA.Fill(resultDS, "Products")
    ' これ以降は非接続でのデータアクセス:
    ' DataSet 内のテーブル"Products"の各レコードに対して処理を実行
    For i = 0 To resultDS.Tables("Products").Rows.Count - 1
      ' ここで何らかのレコード単位での処理を記述
      Diagnostics.Debug.WriteLine(resultDS.Tables("Products").Rows(i).Item("ProductID"). _
      ToString() & "," & esultDS.Tables("Products").Rows(i).Item("ProductName").ToString())
    Next
  Catch E As Exception
    Diagnostics.Debug.WriteLine(E.ToString)
  Finally
  End Try
End Sub

DataAdapter のクラスの Fill メソッドを実行することで Select 文が発行され、結果のレコードが DataSet 内の Table として格納される。Fill メソッドの実行時には、内部的に DBMS とのコネクションが確立し、Select 文が実行され、結果が DataSet の中のテーブルとして格納された後、コネクションが解放される。DataSet は ADO の Recordset と異なり、結果セットの 1 テーブル分のみが格納されるのではなく、必要に応じて複数のテーブルのレコードを格納できる。

スケーラブルなプログラミングモデル

非接続型のデータアクセスも、内部的な動作は接続型のデータアクセスとなんら変わりがない。DataAdapter が内部的に使用するクラスは Connection であり、Command である。違うのは、内部的に行われる DBMS との接続と Select 文の実行が、あくまでデータを DataSet 内部に格納することを目的としている点である。接続型のデータアクセスの場合、データを行単位に読み込みながら処理を逐次実行するので DBMS との接続が常に必要だが、DataAdapter を経由した場合、インメモリの DataSet を使うため、たとえばデータを画面へ表示するための編集処理などに関しては DBMS との接続が断たれていても問題がない。参照系の基本的な動作に関しては接続型データアクセスと非接続型データアクセスとの相違はほとんどないものの、インメモリのデータベースである DataSet で十分対応可能な処理に関しては、DBMS とのコネクションといった貴重なリソースを解放できるという点で非接続型データアクセスの方がスケーラブルなプログラミングモデルとなっている。

データソースに対する非依存性

また、いったん DataSet を作成した後のプログラミングに関しては、そのデータの引用元となった DBMS にはまったく依存しない共通的なデータアクセスが実現する。DataSet 自体で XML ドキュメントの読み込みや、XML ドキュメントとしてのデータ操作が可能なことから、DataSet の元になるデータソースはリレーショナルデータベースに限定されない。インターネットやイントラネットで入手した XML ドキュメントでも、DataSet に格納することでまったく共通したデータアクセスが可能となる。

非接続型のデータ更新

DataSet は最初にレコードを読み込んだときの情報と、それに対してローカルで行われた更新内容を共に保持している。それに加えて、どのような更新が行われたかといった更新状態に関しても保持する。図 1 に DataSet の更新のイメージを示す。

図 1 DataSet 内の更新イメージ

図 1 DataSet 内の更新イメージ

DataSet への更新は、あくまで DB のコピーデータに対する更新であり、このままでは元の DB は更新されない。DataAdapter は、DataSet の更新状態を見て、適切な更新系 SQL を自動的に選択し発行する。図 2 は、SQL の自動発行のイメージである。DataAdapter の内部には、検索、更新、削除、追加の各操作に対応する 4 種類の Command オブジェクトが必要に応じて存在し、各 Command オブジェクト内の CommandText プロパティにそれぞれの動作に応じた SQL 文あるいは、Transact-SQL ステートメントやストアドプロシージャが格納される。DataAdapter の Update メソッドの起動によって、これらのコマンドが自動的に選択実行されて DB への更新が行われる。この更新のための SQL の発行は、DataSet の更新された各行に対応し行単位に実行される。次のリスト 6 は、非接続データアクセスでの更新サンプルである。

図 2 DataAdapter によるSQL 自動発行のイメージ

図 2 DataAdapter によるSQL 自動発行のイメージ

リスト 6 非接続データアクセスでの更新サンプル

Sub Example2_3()
  ' 非接続データアクセスでの更新サンプル
  Dim sqlDA As SqlDataAdapter
  Dim sqlCmdBldr As SqlCommandBuilder
  Dim resultDS As DataSet
  Try
    ' DataAdapter オブジェクトの生成
    sqlDA = New SqlDataAdapter()
    ' DataSet オブジェクトの生成
    resultDS = New DataSet()
    With sqlDA
      ' SelectCommand オブジェクトの追加
      .SelectCommand = New SqlCommand()
      ' SelectCommand の各種プロパティの設定
      With .SelectCommand
        .CommandType = CommandType.Text
        .CommandText = "SELECT * FROM Products"
        .Connection = New SqlConnection(getConnectionString)
      End With
      ' 検索結果をDataSet へ格納
      .Fill(resultDS, "Products")
    End With
    ' インメモリのDataSet に対する更新処理
    resultDS.Tables("Products").Rows(0).Item("ProductName") =
  "Chai_2_3"
  
    ' Update 文の自動生成のためにCommandBuilder オブジェクトを生成
    sqlCmdBldr = New SqlCommandBuilder(sqlDA)
    ' DataSet に対する更新結果をDB へ反映
    sqlDA.Update(resultDS, "Products")
    Diagnostics.Debug.WriteLine("レコードが更新されました")
  Catch E As Exception
    Diagnostics.Debug.WriteLine(E.ToString)
  Finally
  End Try
End Sub

SqlCommandBuilder クラスを利用すると、更新系のSQL 文が自動生成される。Visual Studio .NET のデータアダプタ構成ウィザードにおいても同様の SQL 文が生成される。ここで自動生成される SQL 文には、最初に Fill メソッドで読み込んだ段階のすべての列の値が Where 句の条件として含まれる。これによって楽観的 (オプティミスティック) 同時実行制御を機能的に実現している。DataAdapterのFill() メソッドによって最初にDataSet 内にデータを格納した後に、DB の元のデータがほかで更新された場合、読み込んだ段階の項目値を Where 句に持つ Update 文や Delete 文の更新件数は 0 件となる。この状態は、DataAdapter の Update() メソッドの実行時の DBConcurrencyException が発生することで検出できる。したがって、ほかで更新された場合の対処はこの例外をキャッチすることでハンドリング可能である。なお、この自動生成された更新系 SQL 文をそのまま使用するかどうかは開発者の自由である。たとえば DBMS 側でタイムスタンプ列やバージョン番号列を実装し (タイムスタンプの更新やバージョン番号の更新はトリガとしてあらかじめ実装)、それを Where 句内で主キー列と共に比較対象とするといった方法もある。

最後に、非接続型データアクセスでのトランザクション制御についても説明しておく。非接続型データアクセスでは、DataAdapter の Update メソッドの起動によって、DataSet 内の更新が発生した行に対応してふさわしい SQL 文が行単位で自動発行される。このとき、Commit が行単位で行われるので、たとえば伝票入力の明細行更新で DataAdapter の既定の動作を採用した場合は、ある行は更新が Commit されたが、ある行では更新が異常終了し Rollback されてしまう、といった状況にもなり得る。伝票形式入力の場合は、通常、該当する伝票番号単位でヘッダーと明細をまとめてトランザクション化する。このような要件の場合に、DataAdapter 経由で DBMS のデータを更新するにはトランザクション制御が必要となる。次のリスト 7 は、そのようなトランザクション制御のサンプルコードである。

リスト7 非接続データアクセスでのトランザクション更新のサンプル

Sub Example2_4()
  ' 非接続データアクセスでのトランザクション更新のサンプル
  Dim sqlConn As SqlConnection
  Dim sqlDA As SqlDataAdapter
  Dim sqlCmdBldr As SqlCommandBuilder
  Dim resultDS As DataSet
  Dim sqlTrans As SqlTransaction
  Try
    ' DataAdapter オブジェクトの生成
    sqlDA = New SqlDataAdapter()
    ' DataSet オブジェクトの生成
    resultDS = New DataSet()
    ' Update 文の自動生成のためにCommandBuilder オブジェクトを生成
    sqlCmdBldr = New SqlCommandBuilder(sqlDA)
    
    With sqlDA
      ' SelectCommand オブジェクトの追加
      .SelectCommand = New SqlCommand()
      ' SelectCommand の各種プロパティの設定
      With .SelectCommand
        .CommandType = CommandType.Text
        .CommandText = "SELECT * FROM Products WITH(UPDLOCK) WHERE ProductID=1"
        .Connection = New SqlConnection(getConnectionString)
      End With
    End With
    
    sqlConn = sqlDA.SelectCommand.Connection
    sqlConn.Open()
    sqlTrans = sqlConn.BeginTransaction(IsolationLevel.Serializable)
    sqlDA.SelectCommand.Transaction = sqlTrans
    ' 検索結果をDataSet へ格納
    sqlDA.Fill(resultDS, "Products")
    ' インメモリのDataSet に対する更新処理
    resultDS.Tables("Products").Rows(0).Item("ProductName") = "chai_2_4"
    
    sqlCmdBldr.GetUpdateCommand.Connection = sqlConn
    sqlCmdBldr.GetUpdateCommand.Transaction = sqlTrans
    ' DataSet に対する更新結果をDB へ反映
    sqlDA.Update(resultDS, "Products")
    sqlTrans.Commit()
    Diagnostics.Debug.WriteLine("レコードが更新されました")
  Catch edbc As DBConcurrencyException
    ' 同時実行違反: UpdateCommand によって0 件処理されました。
    ' ここでほかで更新が発生した場合の処理を記述
    Diagnostics.Debug.WriteLine(edbc.ToString)
  Catch esql As SqlException
    Diagnostics.Debug.WriteLine(esql.ToString)
  Catch e As Exception
    Diagnostics.Debug.WriteLine(e.ToString)
  Finally
    If Not sqlTrans.Connection Is Nothing Then
      sqlTrans.Rollback()
    End If
    If Not (sqlConn.State = ConnectionState.Closed) Then
      sqlConn.Close()
    End If
  End Try
End Sub

ADO.NET における接続型データアクセスと非接続型データアクセスのもっとも大きな違いは、非接続状態でデータを処理できるかどうかということだが、DBMS への接続時のデータアクセスにおいては、どちらも本質的な違いはない。

楽観的同時実行制御と悲観的同時実行制御

次は、ADO.NET の同時実行制御について触れておく。同時実行制御の方法は、大きく悲観的同時実行制御と楽観的同時実行制御の2 つに分かれる。悲観的同時実行制御は、ほかでの更新が発生することを抑止するために、データに対してロックをかける。ロックをかけて処理をするため、ほかのユーザーからの更新要求はロックが解除されるまで待たなければならない (この状態が過度に増えると、システム全体の応答性能が低下する。また、DBMS との接続を常に維持しておく必要がある)。これに対して楽観的同時実行制御では、DBMS でロックをかけないため、ほかのユーザーからの同時更新が可能となる、また、実際の更新までの間はデータとの接続を維持しておく必要性がない。したがって、パフォーマンスやスケーラビリティの観点でも有利であると言える。

楽観的同時実行制御の勧め

DBMS をデータの永続化機構として使用した場合 (ほとんどの事務処理系アプリケーションが該当すると思うが)、ビジネスロジック側あるいはプレゼンテーションロジック側では、DB から取得したデータの写しをメモリに一時的に保持して利用する。当然ながらこのデータのコピーは、コピーした直後から陳腐化が始まる。メモリに展開されたデータは、あくまで DB からの読み込み時点での写しである。照会系アプリケーションでこれが問題になることは少ないが、更新系の処理ではそうはいかない。メモリ内のコピーの更新結果を DBMS に反映するには、自ずとバッチ更新的な手法が必要となる。つまり、ユーザーからの更新内容をデータベースに一括転送し更新を反映することとなる。その際に重要な概念となるのが、楽観的同時実行制御である。一例として、ユーザーの入力待ちが発生する対話処理を想定してみよう。ユーザーのオペレーション時間はその画面で何を行うかによって異なるが、一般的には十数秒から、場合によっては数分かかる。十数秒~数分という時間は、千分の一ミリ秒単位で処理をするコンピュータにとっては永遠とも言える長い時間である。このようなユーザーの入力待ち状態を DBMS のロックによって制御することは、実装上は容易だがシステム上は好ましくはない。なぜなら、処理完結までに不特定に時間がかかるのがあらかじめ予想されている場合に、DBMS のセッションやレコード/ページなどのリソースに対するロックを保持し続けるのは、スケーラビリティの低下に直結するからである。サーバーリソースを長期間占有する手法では、要求のたびにサーバーの貴重なリソースが使われっぱなしになり、リソースを有効活用できず非常に無駄が多い。これでは今日の不特定多数のクライアントに対する要求を処理するアプリケーションにとっては致命的である。

従来の汎用機でのアプリケーションにおいても、処理の途中にユーザーとの対話が入る場合は、DB への更新前に最初に読み込んできた内容と、現在 DB に格納されている内容が本当に同じかどうかチェックする必要があった。このチェックによってほかのユーザーからの更新が発生していたかどうかを事前にチェックし、更新が発生していなければ実際に更新を行い、更新が発生していれば再度最新情報を読み込み、更新をやり直すように入力者に促す。これは汎用機におけるスケーラビリティ確保の鉄則であり、このような処理形態にすることで貴重なシステムリソースの無意味な消費を抑えていた。しかし、2 階層クライアント/サーバーにおいてはこのセオリーが忘れさられた感がある。たとえば、DBMS が提供するロック機能をユーザーとの対話処理をまたがって使っているため、ほかのユーザーが該当データへアクセスができないようになっている場合がある。しかし、このような実装方法は、先に説明したとおり、今日のインターネットあるいはイントラネットでの不特定多数をサポートするようなシステム形態においては好ましくない。3 階層クライアント/サーバーや Web アプリケーション、あるいは XML Web サービスを実装する場合には、必ず要求のたびにロックを解放すべきである。ロック制御を自作せざるを得ない場合もあるが、実装は複雑になりがちで、管理面での不安も残る。したがって、ユーザー入力待ちの対処方法は、基本的には楽観的同時実行制御での対処がもっとも実装しやすいと言える。また、楽観的同時実行制御でも十分対処可能な業務もある。たとえば伝票の入力や修正の場合、同一伝票に対する更新処理は、該当伝票を見ながらのオペレーションがほとんどであるので、基本的には 1 人のオペレータが更新を行う (同一伝票を複数人が同時に更新するような状況は、基本的にはオペレーションミスである)。

楽観的同時実行制御での問題点

楽観的同時実行制御で問題となるのは、入力/更新対象のレコードに対してバックグラウンドで大量のバッチ更新が発生する場合だが、これも業務運用の設計しだいでバッチ更新時の個別のデータ修正を抑止したり、運用時間帯を明確に分離することで対処が可能である。更新衝突時の対処方法に関しては、機能的なユーザー要求のレベルによっては、非常に高度な要求もあり得る。たとえば、先行する更新処理の更新対象項目と、後続の更新処理の更新対象項目が場合によってはまったく異なっている場合もあり得る。この場合、業務的な矛盾が存在しなければ、後続の更新処理に関しては再入力の必要性をなくすことが可能である。また、ほかで更新中のエラーが発生した場合に関しては、入力/修正された内容に関して再度読み込んだ最新の情報とマージ可能なデータをマージすることで、再入力の手間を省くといった入力支援機能を付加する必要があるかもしれない。

楽観的同時実行制御と悲観的同時実行制御の使いどころ

楽観的同時実行制御と悲観的同時実行制御の使い分けであるが、ユーザー入力待ちやほかのシステムとの連携などが一連の処理中に入る場合はもちろんのこと、多階層システムにおいては基本的には楽観的同時実行制御を選択し、実際の DB への最終的な更新を行うタイミングのみ業務要件に応じて悲観的同時実行制御+トランザクション制御によって実装するのが現実的であると言える。なお、DataSet の DataAdapter 経由の更新においては、非接続データアクセスと楽観的同時実行制御が基本となっており、それをベースにシステム実装を図ると、自ずとスケーラビリティの向上が図れる。

まとめ

今回は、接続型データアクセスと非接続型データアクセスの相違点と共通点、DataAdapter を経由した DataSet における DB への更新反映メカニズム、そして楽観的同時実行制御と悲観的同時実行制御の使い分けについて採り上げた。次回以降では、非接続型データアクセスと接続型データアクセスの用途に応じた使い分けと、データアクセス層の分離 (論理的および物理的な分離) に関してみていきたい。

参考資料

Visual Studio .NET Document :
ADO.NET における同時実行制御
データセットのレコードの更新、挿入、および削除
データソースへのデータセット変更内容の書き込み

MSDN :
.NET DataAccess Architecture Guide
Technical Notes and Examples にあるAccessing Data のリンク