実環境での問題(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コマンドが必要な場合は、このチェックをオフにすることができます。これは新しいSQL UPDATEコマンドを生成するときの設計時アクションです。オプティミスティックな同時実行制御チェックのオン/オフを切り替えるにはデータセットデザイナを使用します。- pubsDataSet.xsdをダブルクリックします。
- titlesTableAdapter右クリックし、[Configure]を選択します。
- [Advanced Options]をクリックします。
- [Use optimistic concurrency]の選択を解除します。
- [OK]をクリックします。
- [Finish]をクリックします。
[Advanced Option]ダイアログを見ると同時実行時の不一致を防ぐことができるように思えますが、実際には検出しかできません。同時実行時の不一致が検出されると、
System.Data.DBConcurrencyExceptionがスローされます。不一致をキャッチして処理することは、開発者の役目であることに変わりありません。- Shift+Alt+Dキーを押して[Data Sources]ウィンドウを開くか、[Data]メニューから[Show Data Sources]を選択します。
- [Add New Data Source]をクリックします。
- [Database]を選択し[Next]をクリックします。
- 定義済みの既存の接続がある場合は、その接続を選択します。既存の接続がない場合は、[New Connection]をクリックして新しい接続を作成し、SQL ServerからPubsデータベースを選択します。
- [Next]をクリックします。
- オブジェクトのリストでTablesノードを展開します。
- titlesテーブルを選択します。
- データセット名がpubsDataSetであることを確認します。
- [Finish]をクリックします。
メインプログラムは、型指定されたテーブルアダプタを使って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コマンドの処理方法は通常と同じなので省略しました。
''' <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を参照)は、テーブル内のすべての列を取得して別々の更新グループに分けます。これは、読み取り専用の列と、通常は更新することができない主キーの列を除外するための絶好のポイントです。
''' <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
リスト3のCreateUpdateCommand()関数は、フィールドのグループごとに1つのSQL UPDATEコマンドを作成します。SQL WHERE句は、行の主キーと更新する必要があるフィールドから成り立ちます。フィールドが同じ値で上書きされても問題にはならないので、CreateUpdateCommand()関数は、各フィールドを古い値および新しい値と比較し、2人のユーザーによる同じ変更が競合と見なされないようにします。
''' <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
おわりに
ここで紹介した手法は、すべての更新の同時実行問題に対応する完全なソリューションではありませんが、正しい方向に向かう一歩だと思います。この取り組みは現在進行形なので、この先、個々のケースに対処する最適な方法を見つける人も出てくるでしょう。
このソリューションが、使いやすく、あまりテクノロジ指向ではない性質のアプリケーションの作成に役立つことを願います。
