SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

japan.internet.com翻訳記事

実環境におけるデータベース更新の競合に対処する

ユーザーの観点から見たデータベース更新の競合解決

実環境での問題(2/2)

 次のMain()メソッドで、更新の仕組みを示す簡単な例を紹介します。この例では、ExtendedProperties列にフィールドのグループ化情報を格納しています。同じグループ名を持つ列は1つのSQL UPDATEコマンドにまとめられます。

''' <summary>
''' Test application to check database updates.
''' </summary>
''' <remarks></remarks>
Sub Main()
   Dim dta As New pubsDataSetTableAdapters.titlesTableAdapter
   Dim table As pubsDataSet.titlesDataTable
   Dim cb As New CommandBuilder

   ' Load the data
   table = dta.GetData()

   ' Configure the table
   cb.ConfigureDataTable(table)

   ' Make a change to the data
   table.Item(0).price *= 1.1

   'table.Item(0).title = "New title"
   ' Update the database
   cb.UpdateTable(table)
   Console.WriteLine( _
      "Press any key to terminate the application.")
   Console.ReadKey()
End Sub
型指定されたデータセットとSQL UPDATEコマンド
 Visual Studio 2005で型指定されたデータセット(typed dataset)を生成する場合、既定ではオプティミスティックな同時実行制御が使用されます。問題のテーブルにタイムスタンプ列がある場合は、このタイムスタンプを使って行バージョンを比較します。問題のテーブルにタイムスタンプ列がない場合は、データセットデザイナはすべてのフィールドのチェックを追加して同時実行時のエラーを検出します。
 同時実行チェックのないSQL UPDATEコマンドが必要な場合は、このチェックをオフにすることができます。これは新しいSQL UPDATEコマンドを生成するときの設計時アクションです。オプティミスティックな同時実行制御チェックのオン/オフを切り替えるにはデータセットデザイナを使用します。
  1. pubsDataSet.xsdをダブルクリックします。
  2. titlesTableAdapter右クリックし、[Configure]を選択します。
  3. [Advanced Options]をクリックします。
  4. [Use optimistic concurrency]の選択を解除します。
  5. [OK]をクリックします。
  6. [Finish]をクリックします。
 以上のステップに従うと、同時実行チェックのないtitlesTableAdapterが再生成されます。
 [Advanced Option]ダイアログを見ると同時実行時の不一致を防ぐことができるように思えますが、実際には検出しかできません。同時実行時の不一致が検出されると、System.Data.DBConcurrencyExceptionがスローされます。不一致をキャッチして処理することは、開発者の役目であることに変わりありません。
型指定されたpubsDataSetを作成する
 サンプルコードでは、型指定されたデータセットを使ってSQL Server Pubsデータベースにアクセスしています。型指定されたデータセットを作成すれば、データベースから迅速、かつ簡単にデータを取得できます。次のステップに従ってpubsDataSetを作成します。
  1. Shift+Alt+Dキーを押して[Data Sources]ウィンドウを開くか、[Data]メニューから[Show Data Sources]を選択します。
  2. [Add New Data Source]をクリックします。
  3. [Database]を選択し[Next]をクリックします。
  4. 定義済みの既存の接続がある場合は、その接続を選択します。既存の接続がない場合は、[New Connection]をクリックして新しい接続を作成し、SQL ServerからPubsデータベースを選択します。
  5. [Next]をクリックします。
  6. オブジェクトのリストでTablesノードを展開します。
  7. titlesテーブルを選択します。
  8. データセット名がpubsDataSetであることを確認します。
  9. [Finish]をクリックします。
 これで、プロジェクト内にファイル「pubsDataSet.xsd」が表示されるはずです。このスキーマを作成すると、「pubsDataSet.Designer.vb」という分離コードのソースファイルも生成されます。ソリューションエクスプローラで[Show All Files]をクリックすると、このファイルが表示されます。このVisual Basicファイルには、Pubsのtitlesテーブル内のデータへの型指定されたアクセスを提供する数多くの生成されたクラスが含まれます。

 メインプログラムは、型指定されたテーブルアダプタを使ってPubsデータベースからtitlesテーブルを読み込みます。他の関数はすべて、メイン関数で作成されたCommandBuilderクラスの一部です。

 次のConfigureDataTable()関数で、各列はUpdateGroup拡張プロパティを受け取ります。これを更新中に使って、どのフィールドをグループ化するかを判別します。この情報は更新時には決定されません。なぜなら、フィールドのグループ化が異なるさまざまなビジネスオブジェクトで、同じテーブルを複数の目的に使う可能性があるからです。さらに、フィールドの中には、このサンプルコードが考慮に入れない読み取り専用のフィールドがある可能性もあります。

''' <summary>
''' Configure the columns into update groups.
''' </summary>
''' <param name="table">The table with columns.</param>
''' <remarks></remarks>
Public Sub ConfigureDataTable( _
   ByVal table As pubsDataSet.titlesDataTable)
   ' Basic data about the book
   table.title_idColumn.ExtendedProperties("UpdateGroup") = "Book"
   table.titleColumn.ExtendedProperties("UpdateGroup") = "Book"
   table.typeColumn.ExtendedProperties("UpdateGroup") = "Book"
   table.notesColumn.ExtendedProperties("UpdateGroup") = "Book"
   table.pubdateColumn.ExtendedProperties("UpdateGroup") = "Book"

   ' Financial data about the book
   table.pub_idColumn.ExtendedProperties("UpdateGroup") = _
      "Financial"
   table.priceColumn.ExtendedProperties("UpdateGroup") = _
      "Financial"
   table.advanceColumn.ExtendedProperties("UpdateGroup") = _
      "Financial"
   table.royaltyColumn.ExtendedProperties("UpdateGroup") = _
      "Financial"

   ' Sales information about the book
   table.ytd_salesColumn.ExtendedProperties("UpdateGroup") = _
      "Sales"
End Sub

 UpdateTable()関数(リスト1を参照)は、まずSQL UPDATEコマンドのコレクションを取得します。この例では、SQL INSERTコマンドとSQL DELETEコマンドの処理方法は通常と同じなので省略しました。

リスト1 UpdateTable()関数
''' <summary>
''' Sends all updates to the database
''' </summary>
''' <param name="table">The table with changes,</param>
''' <returns></returns>
''' <remarks>Just demo code.
''' Cannot execute as there is no connection and Insert/Delete
''' is not implemented.
''' </remarks>
Public Function UpdateTable(ByVal table As DataTable) As Boolean
   Dim updateCommands As List(Of SqlCommand)

   ' Get a list of update commands to execute
   updateCommands = GetUpdateCommands(table)
   For Each row As DataRow In table.GetChanges(DataRowState.Modified).Rows()
      Select Case row.RowState
         Case DataRowState.Added
            ' New row, do an database insert
         Case DataRowState.Deleted
            ' Deleted row, do a database delete
         Case DataRowState.Modified
            ' Changed row, do the required database updates
            For Each cmd As SqlCommand In updateCommands
               Dim hasChanges As Boolean = False
               For Each param As SqlParameter In _
                  cmd.Parameters()
                  ' Populate all parameters
                  Dim fieldName As String
                  fieldName = param.ParameterName.Substring(3)
                  If param.ParameterName. StartsWith("old") Then
                     param.Value = row(fieldName, DataRowVersion.Original)
                  Else
                     param.Value = row(fieldName, DataRowVersion.Current)
                  End If
                  ' Check if this field is changed
                  hasChanges = hasChanges OrElse Not row(fieldName, _
                     DataRowVersion.Original). Equals(row(fieldName, _
                     DataRowVersion.Current))
               Next
               If hasChanges Then Console.ForegroundColor = _
                  ConsoleColor.Yellow
                  Console.WriteLine( "Executing command:")
                  'cmd.ExecuteScalar()
               Else
                  Console.ForegroundColor = ConsoleColor.Red
                  Console.WriteLine("Skipping command:")
               End If
               Console.WriteLine(cmd.CommandText)
               Console.WriteLine()
               Console.ResetColor()
            Next
      End Select
   Next
End Function

 次のコードは、GetUpdateCommands()関数がすべてのフィールドグループ内を反復処理し、フィールドグループごとに別々のSQL UPDATEコマンドを作成する方法を示しています。個々のコマンドは、すべて1つのコレクションにまとめられてコレクションが返されます。

''' <summary>
''' Build a collection of update commands for the table.
''' </summary>
''' <param name="table">
''' The table that needs to be updated.</param>
''' <returns>
''' A collection of SQLCommands for the update.</returns>
''' <remarks></remarks>
Private Function GetUpdateCommands(ByVal table As DataTable) _
   As List(Of SqlClient.SqlCommand)
   Dim groups As IDictionary(Of String, List(Of DataColumn))
   Dim cmds As List(Of SqlClient.SqlCommand)

   cmds = New List(Of SqlClient.SqlCommand)
   Console.WriteLine("Building update commands.")
   Console.WriteLine()

   ' Split all columns into groups based upon the
   ' UpdateGroup extended property.
   groups = SplitColumnIntoGroups(table)
   For Each group As List(Of DataColumn) In groups.Values
      Dim cmd As SqlCommand
      cmd = CreateUpdateCommand(table, group)
      cmds.Add(cmd)
      Console.WriteLine("Update command {0}:", cmds.Count)
      Console.WriteLine(cmd.CommandText)
      Console.WriteLine()
   Next
   Return cmds
End Function

 SplitColumnIntoGroups()関数(リスト2を参照)は、テーブル内のすべての列を取得して別々の更新グループに分けます。これは、読み取り専用の列と、通常は更新することができない主キーの列を除外するための絶好のポイントです。

リスト2 SplitColumnIntoGroups()関数
''' <summary>
''' Split all columns into groups based upon the UpdateGroup
''' extended property.
''' </summary>
''' <param name="table">
''' The table with columns to split.</param>
''' <returns>A dictionary with the groups of columns.</returns>
''' <remarks></remarks>
Private Function SplitColumnIntoGroups(ByVal table As DataTable) _
   As Dictionary(Of String, List(Of DataColumn))
   Dim groups As New Dictionary(Of String, List(Of DataColumn))

   For Each col As Data.DataColumn In table.Columns
      Dim updateGroup As String
      If col.ExtendedProperties.Contains("UpdateGroup") Then
         updateGroup = col.ExtendedProperties("UpdateGroup").ToString()
      Else
         updateGroup = ""
      End If
      If Not groups.ContainsKey(updateGroup) Then
         groups.Add(updateGroup, New List(Of DataColumn))
      End If
      groups(updateGroup).Add(col)
   Next
   Return groups
End Function

 リスト3CreateUpdateCommand()関数は、フィールドのグループごとに1つのSQL UPDATEコマンドを作成します。SQL WHERE句は、行の主キーと更新する必要があるフィールドから成り立ちます。フィールドが同じ値で上書きされても問題にはならないので、CreateUpdateCommand()関数は、各フィールドを古い値および新しい値と比較し、2人のユーザーによる同じ変更が競合と見なされないようにします。

リスト3 CreateUpdateCommand()関数
''' <summary>
''' Create a SqlCommand to update the field group.
''' </summary>
''' <param name="table">The table being updated.</param>
''' <param name="group">The field group.</param>
''' <returns>The SqlCommand to update the table.</returns>
''' <remarks></remarks>
Private Function CreateUpdateCommand(ByVal table As DataTable, _
   ByVal group As IEnumerable(Of DataColumn)) As SqlCommand

   ' Build an update command for the group of columns
   Dim cmd As New Data.SqlClient.SqlCommand
   Dim sqlSet As New System.Text.StringBuilder()
   Dim sqlWhere As New System.Text.StringBuilder()
   For Each col As DataColumn In table.PrimaryKey
      If sqlWhere.Length > 0 Then
         sqlWhere.Append(" and ")
      End If
      sqlWhere.Append("([")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append("] = @org")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append(" or [")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append("] = @new")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append(")")
      cmd.Parameters.AddWithValue("old" + col.ColumnName, col.DataType)
      cmd.Parameters.AddWithValue("new" + col.ColumnName, col.DataType)
   Next
   For Each col As DataColumn In group
      If sqlSet.Length > 0 Then
         sqlSet.Append(", ")
      End If
      sqlSet.Append("[")
      sqlSet.Append(col.ColumnName)
      sqlSet.Append("] = @new")
      sqlSet.Append(col.ColumnName)
      If sqlWhere.Length > 0 Then
         sqlWhere.Append(" and ")
      End If
      sqlWhere.Append("([")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append("] = @org")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append(" or [")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append("] = @new")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append(")")
      If Not cmd.Parameters.Contains("old" + col.ColumnName) Then
         cmd.Parameters.AddWithValue("old" + col.ColumnName, _
            col.DataType)
      End If
      If Not cmd.Parameters.Contains("new" + col.ColumnName) Then
         cmd.Parameters.AddWithValue("new" + col.ColumnName, _
            col.DataType)
      End If
      Dim commandText As String
      commandText = "Update [{0}] Set {1} Where ({2})"
      cmd.CommandText = String.Format(commandText, _
         table.TableName, sqlSet.ToString(), sqlWhere.ToString())
   Next
   Return cmd
End Function

おわりに

 ここで紹介した手法は、すべての更新の同時実行問題に対応する完全なソリューションではありませんが、正しい方向に向かう一歩だと思います。この取り組みは現在進行形なので、この先、個々のケースに対処する最適な方法を見つける人も出てくるでしょう。

 このソリューションが、使いやすく、あまりテクノロジ指向ではない性質のアプリケーションの作成に役立つことを願います。

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
japan.internet.com翻訳記事連載記事一覧

もっと読む

この記事の著者

japan.internet.com(ジャパンインターネットコム)

japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.comEarthWeb.com からの最新記事を日本語に翻訳して掲載するとともに、日本独自のネットビジネス関連記事やレポートを配信。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

Maurice de Beijer(Maurice de Beijer)

フリーのソフトウェア開発者、ベータテスタ、そしてMicrosoftのMVP受賞者である。.NET、オブジェクト指向、Visual FoxProのほか、技術的に難しい問題の解決を専門とする。「The Problem Solver」というサイト(www.TheProblemSolver.nl)を運営している。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/697 2006/11/14 00:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー