記事の第2部では、LINQ to SQLを使用する際のこの競合の解決に専念します。
記事の第2部では、LINQ to SQLでの同時アクセスの競合を解決する方法、レコードを更新しようとしたときのChangeConflictExceptionの原因、およびその解決方法について説明します。
競合検出
記事の第1部で既に述べたように、バージョンまたは変更の日付が保存される特別なバージョンフィールドを使用するか、WHERE句で以前のフィールド値を指定して、競合を見つけるために変更されていないことを確認できます。
バージョンフィールドを使用するには、このフィールドを、テーブルで説明されているエンティティクラス(* .designer.cs)のColumn属性のIsVersionプロパティでマークする必要があります。 この場合、このフィールドのみが関与して、並列アクセス競合の発生を判断します。
IsVersionとしてマークされているフィールドがない場合、LINQ to SQLを使用すると、競合検出に関与するフィールドを自分で制御できます。 これを行うには、エンティティクラスで、Column属性のUpdateCheckプロパティの対応する値を設定する必要があります。 次の3つの値が可能です。
- なし-このフィールドが競合検出に参加しないことを示します
- Always(デフォルト)-キャッシュへのDataContextオブジェクトの最初のロード以降に値が更新されたかどうかに関係なく、このフィールドは常に競合検出に参加します。
- WhenChanged-このフィールドは、更新して新しい値を保存しようとすると参加します
それでは、実用的な部分に移りましょう。 最初に、テストテーブルを作成します。
CREATE TABLE Customers ([CustomerID][nvarchar](5) PRIMARY KEY , [CompanyName][nvarchar](40), [ContactName][nvarchar](30), [ContactTitle][nvarchar](30))
* This source code was highlighted with Source Code Highlighter .
次に、Visual Studioのプロジェクトに移動して、テーブルをプロジェクトに追加します(新しい項目の追加-LinqをSqlクラスに追加)。
ファイルMyTestDB.designer.csを開き、クラスの説明に進みます
[global::System.Data.Linq.Mapping.TableAttribute(Name= "dbo.Customers" )]
public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged
* This source code was highlighted with Source Code Highlighter .
すべてのフィールドとそのプロパティを説明するだけです
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_CustomerID" , DbType= "NVarChar(5) NOT NULL" , CanBeNull= false , IsPrimaryKey= true )]
public string CustomerID …
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_CompanyName" , DbType= "NVarChar(40)" )]
public string CompanyName …
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_ContactName" , DbType= "NVarChar(30)" )]
public string ContactName …
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_ContactTitle" , DbType= "NVarChar(30)" )]
public string ContactTitle…
* This source code was highlighted with Source Code Highlighter .
すでに書いたように、UpdateCheckプロパティは設定されていませんが、デフォルト値はAlwaysです。 これは、すべてのフィールドが検証に関与することを意味します。 それを確認しましょう。 次のコードを記述します。
MyTestDBDataContext db = new MyTestDBDataContext();
db.Log = Console .Out;
Customer cust = db.Customers.Where(c => c.CustomerID == "LONEP" ).SingleOrDefault();
cust.ContactName = "Neo Anderson" ;
db.SubmitChanges();
* This source code was highlighted with Source Code Highlighter .
起動後、コンソールウィンドウに次のように表示されます。
SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle]
FROM [dbo].[Customers] AS [t0]
WHERE [t0].[CustomerID] = @p0
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
UPDATE [dbo].[Customers]
SET [ContactName] = @p4
WHERE ([CustomerID] = @p0) AND ([CompanyName] = @p1) AND ([ContactName] = @p2) AND ([ContactTitle] = @p3)
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- @p1: Input NVarChar (Size = 24; Prec = 0; Scale = 0) [Lonesome Pine Restaurant]
-- @p2: Input NVarChar (Size = 11; Prec = 0; Scale = 0) [Fran Wilson]
-- @p3: Input NVarChar (Size = 13; Prec = 0; Scale = 0) [Sales Manager]
-- @p4: Input NVarChar (Size = 12; Prec = 0; Scale = 0) [Neo Anderson]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
つまり ここでは、LINQ to SQLがクエリを変換して実行する方法を確認します。 更新では、更新中にすべてのフィールドが競合の検出に参加したことがはっきりとわかります。 これが必要ない場合は、UpdateCheckプロパティの値を手動で設定して制御できます。 たとえば、CompanyNameは常に競合検出に関係し、ContactNameは変更時のみ、ContactTitleは決して関係しないことを確認できます。 次に、このようになります:
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_CustomerID" , DbType= "NVarChar(5) NOT NULL" , CanBeNull= false , IsPrimaryKey= true )]
public string CustomerID …
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_CompanyName" , DbType= "NVarChar(40)" , UpdateCheck = UpdateCheck.WhenChanged)]
public string CompanyName …
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_ContactName" , DbType= "NVarChar(30) " , UpdateCheck = UpdateCheck.Always)]
public string ContactName …
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_ContactTitle" , DbType= "NVarChar(30)" , UpdateCheck = UpdateCheck.Never)]
public string ContactTitle…
* This source code was highlighted with Source Code Highlighter .
また、コードを再実行すると、Linq to Sqlクエリはすでに異なります。
SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle]
FROM [dbo].[Customers] AS [t0]
WHERE [t0].[CustomerID] = @p0
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
UPDATE [dbo].[Customers]
SET [ContactName] = @p3
WHERE ([CustomerID] = @p0) AND ([CompanyName] = @p1) AND ([ContactName] = @p2)
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- @p1: Input NVarChar (Size = 24; Prec = 0; Scale = 0) [Lonesome Pine Restaurant]
-- @p2: Input NVarChar (Size = 11; Prec = 0; Scale = 0) [Fran Wilson]
-- @p3: Input NVarChar (Size = 12; Prec = 0; Scale = 0) [Neo Anderson]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
ご覧のとおり、違いがあります! CompanyNameフィールドは参加せず、ContactNameは変更されたため参加しました。
したがって、SubmitChanges()が呼び出されたときに競合の競合検出が発生し、競合が発生した場合は、競合解決プロセスを制御できます。 最初の競合中にさらなる更新を中断するか、競合を蓄積してすべての変更を試みるかを指定できます。 これを行うには、SubmitChangesを呼び出すときにConflictModeを渡す必要があります。 ConflictMode.FailOnFirstConflictを渡すと、最初の競合でプロセスが中断されます。別の値はConflictMode.ContinueOnConflictです。 デフォルトでは、指定しない場合、ConflictMode.FailOnFirstConflictが使用されます。
トランザクションを指定したかどうかに関係なく(この場合、データベースを変更するすべての試行に対して作成されます)、例外がスローされると、トランザクションはキャンセルされます。 これは、一部の変更が正常に更新された場合、エラーが発生するとそれらがキャンセルされることを意味します。
ChangeConflictException例外
ConflictModeがFailOnFirstExceptionまたはContinueOnConflictに設定されているかどうかに関係なく、ChangeConflictException例外は引き続きスローされます。
この例外をキャッチすると、競合の発生を検出できます。
紛争解決
競合が見つかったらすぐに、ChangeConflictException例外をキャッチします。次のステップでは、おそらくそれを解決します。 LINQ to SQLには、2つのResolveAllメソッドと2つのResolveメソッドがあります。
リフレッシュモード
ResolveAllまたはResolveメソッドを呼び出して組み込みのLINQ to SQL機能を使用して競合を解決する場合、RefreshModeモードを指定して競合解決メソッドを制御します。 有効な値は3つあります。
- KeepChangesは、ResolveAllまたはResolveメソッドに、データベースからエンティティクラスのプロパティを更新するよう指示しますが、ユーザーがプロパティを変更しても、その値は保存されます(つまり、変更は失われません)
- KeepCurrentValuesは、ユーザーの変更を使用する必要があることを示し、データベースで行われたすべての変更を拒否します(データベースの値は、ブート時に読み取られたものによって上書きされます)
- OverwriteCurrentValuesは、変更を破棄してデータベースから値をロードする必要があることを意味します
紛争解決アプローチ
競合解決には、シンプル、簡単、手動の3つのアプローチがあります。 最も簡単なのは、RefreshModeと、削除されたレコードを自動的に削除するかどうかを示すオプションのブール値を使用して、DataContext.ChangeConflictsコレクションのResolveAllメソッドを呼び出すことです(この場合、LINQ to SQLは、削除されるレコードが正常に削除されたことを意味します何かが私たちの前にそれらを削除しました)
簡単な方法は、DataContext.ChangeConflictsコレクションのすべてのobjectChangeConflictを、それぞれのResolveメソッドを呼び出してリストすることです。
手動の方法では、DataContextオブジェクトのChangeConflicts要素を列挙し、ObjectChangeConflictsオブジェクトのすべてのMemberConflicts要素を列挙して、このコレクションの各MemberChangeConflictオブジェクトでResolveを呼び出します。
DataContext.ChangeConflicts.ResolveAll()
紛争の解決は難しくありません。 ChangeConflictExceptionをキャッチし、DataContext.ChangeConflictsコレクションでResolveAll()メソッドを呼び出すだけです。 必要なことは、使用するRefreshModeモードと、削除されたレコードの競合を自動的に解決するかどうかを決定することだけです。 このアプローチを適用すると、すべての競合が同じように解決されます。
cust.ContactName = "Neo Anderson" ;
try
{
db.SubmitChanges();
}
catch (ChangeConflictException)
{
db.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges);
{
try
{
db.SubmitChanges();
}
catch (ChangeConflictException)
{
Console .WriteLine( " , ." );
}
}
}
* This source code was highlighted with Source Code Highlighter .
この例では、まずResolveAllを呼び出してから、SubmitChangesメソッドを再度呼び出します。 エラーが発生した場合、ロールバックします。
ObjectChangeConflict.Resolve()
同じRefreshModeまたはautoResolveDeletesを使用した競合の解決がうまくいかない場合は、DataContext.ChangeConflictsコレクションからすべての競合をリストするアプローチを選択し、それらを個別に処理できます。
cust.ContactName = "Neo Anderson" ;
try
{
db.SubmitChanges();
}
catch (ChangeConflictException)
{
foreach (ObjectChangeConflict conflict in db.ChangeConflicts)
{
Console .WriteLine( " {0}" , ((Customer)conflict.Object).CustomerID);
conflict.Resolve(RefreshMode.KeepChanges);
Console .WriteLine( " . {0}" , System.Environment.NewLine);
}
try
{
db.SubmitChanges();
}
catch (ChangeConflictException)
{
Console .WriteLine( " , ." );
}
}
}
* This source code was highlighted with Source Code Highlighter .
ここでは、ResolveAllメソッドと同様に、ChangeConflictsコレクションを列挙し、各ObjectChangeConflictでResolveを呼び出します。
MemberChangeConflict.Resolve()
手動の競合解決の例では、データベースのContactName列と競合がある場合、コードはデータベースの値をそのままにする必要がありますが、レコード内の他の列は更新できるという要件があるとします。
これを実装するには、同じ基本的なアプローチを使用しますが、ObjectChangeConflictオブジェクトでResolveを呼び出す代わりに、各オブジェクトのMemberConflictsコレクションのメンバーを列挙します。 次に、このコレクションの各MemberConflictオブジェクトについて、競合の原因となったエンティティのプロパティがContactNameの場合、RefreshMode.OverwriteCurrentValuesをResolveメソッドに渡すことにより、データベースに値を残します。 競合するプロパティがContactNameでない場合は、RefreshMode.KeepChanges値をResolveメソッドに渡すことで、プロパティを更新します。
cust.ContactName = "Neo Anderson" ;
cust.CompanyName = "Lonesome & Pine Restaurant" ;
try
{
db.SubmitChanges();
}
catch (ChangeConflictException)
{
foreach (ObjectChangeConflict conflict in db.ChangeConflicts)
{
Console .WriteLine( " {0}" , ((Customer)conflict.Object).CustomerID);
foreach (MemberChangeConflict memberConflict in conflict.MemberConflicts)
{
if (memberConflict.Member.Name.Equals( "ContactName" ))
memberConflict.Resolve(RefreshMode.OverwriteCurrentValues);
else
memberConflict.Resolve(RefreshMode.KeepChanges);
}
Console .WriteLine( " . {0}" , System.Environment.NewLine);
}
try
{
db.SubmitChanges();
}
catch (ChangeConflictException)
{
Console .WriteLine( " , ." );
}
}
}
* This source code was highlighted with Source Code Highlighter .
競合の解決を手動で処理する有効なコードはそれほど悪くありません。 しかし、もちろん、これらの努力はすべて、専門的な紛争解決のためにのみ正当化されます。
更新:バージョン列を使用
[バージョン]列には、タイムスタンプまたはintを使用できます。 たとえば、レコードのバージョンを追跡するテーブルに列を追加しましょう。
alter table Customers ADD [Version][rowversion] NOT NULL
* This source code was highlighted with Source Code Highlighter .
rowversion-レコードを更新するときに自動的に更新されるフィールド;内部では、タイムスタンプが使用されます。 つまり LINQ to SQLなどを使用して更新する方法に関係なく、フィールドの値は自動的に増加します。 もちろん、別の方法を使用してバージョン番号を保持することもできます。たとえば、番号として、トリガーを使用して更新できます。 この場合、たとえば、rowversionタイプを使用しました
次に、この新しいフィールドがプロジェクトに表示されるように、ビューMyTestDB.dbmlを更新する必要があります。 更新後、フィールドは次のようになります。
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_Version" , AutoSync=AutoSync.Always, DbType= "rowversion NOT NULL" , CanBeNull= false , IsDbGenerated= true , IsVersion= true , UpdateCheck=UpdateCheck.Never)]
public System.Data.Linq.Binary Version
* This source code was highlighted with Source Code Highlighter .
IsVersion = trueを使用すると、UpdateCheckを忘れることがありますが、適用されません!!! バージョンフィールドは、競合の検索に常に使用されます。 その他のプロパティ:IsDbGenerated-このフィールドはデータベースによって生成され、変更できないことを示します。CanBeNull-空の値を許可しません。 IsVersionフィールドは、レコードを挿入または更新した直後の同期を想定しています
そして、バージョンフィールドを実装した後、次のコードを使用して、LINQ to SQLクエリがどのように変更されたかを確認しましょう。
MyTestDBDataContext db = new MyTestDBDataContext();
db.Log = Console .Out;
Customer cust = db.Customers.Where(c => c.CustomerID == "LONEP" ).SingleOrDefault();
string name = cust.ContactName;
cust.ContactName = "Neo Anderson" ;
Console .WriteLine( " - {0} {1}" , BitConverter.ToString(cust.Version.ToArray()), System.Environment.NewLine);
db.SubmitChanges();
Console .WriteLine( " - {0} {1}" , BitConverter.ToString(cust.Version.ToArray()), System.Environment.NewLine);
cust.ContactName = name;
db.SubmitChanges();
Console .ReadKey();
* This source code was highlighted with Source Code Highlighter .
ここでは、サンプルを数回実行できるように2、3行を追加しました(上記の前の例では、自分で使用しましたが、サンプルに追加しませんでした)。 これにより、サンプルを数回実行し、常に結果を確認できます。 キャッシュ後にデータが変更されていない場合、更新のためにデータベースに送信されないためです!
そのため、バージョンが実際に更新されていることを確認するために、バージョン番号の表示も追加しました。 結果は以下のとおりです。
SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Version]
FROM [dbo].[Customers] AS [t0]
WHERE [t0].[CustomerID] = @p0
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
- 00-00-00-00-00-00-07-E9
UPDATE [dbo].[Customers]
SET [ContactName] = @p2
WHERE ([CustomerID] = @p0) AND ([Version] = @p1)
SELECT [t1].[Version]
FROM [dbo].[Customers] AS [t1]
WHERE ((@@ROWCOUNT) > 0) AND ([t1].[CustomerID] = @p3)
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- @p1: Input Timestamp (Size = 8; Prec = 0; Scale = 0) [SqlBinary(8)]
-- @p2: Input NVarChar (Size = 12; Prec = 0; Scale = 0) [Neo Anderson]
-- @p3: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
- 00-00-00-00-00-00-07-EA
UPDATE [dbo].[Customers]
SET [ContactName] = @p2
WHERE ([CustomerID] = @p0) AND ([Version] = @p1)
SELECT [t1].[Version]
FROM [dbo].[Customers] AS [t1]
WHERE ((@@ROWCOUNT) > 0) AND ([t1].[CustomerID] = @p3)
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- @p1: Input Timestamp (Size = 8; Prec = 0; Scale = 0) [SqlBinary(8)]
-- @p2: Input NVarChar (Size = 11; Prec = 0; Scale = 0) [Fran Wilson]
-- @p3: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
ご覧のとおり、バージョンフィールドのみが競合の検索に使用されます。 さらに、更新後、すぐにデータベースと同期され、現在の値を受け取ります。
例外のキャッチと処理は、上記の例とまったく同じです。
最後のUPDATEおよびSELECTは、元の値を復元しているためです。
悲観的なアプローチ
並列処理に対する悲観的なアプローチでは、データベースがトランザクションによってブロックされているため、解決する必要のある競合はありません。
using (System.Transactions.TransactionScope tranaction = new System.Transactions.TransactionScope())
{
Customer cust = db.Customers.Where(c => c.CustomerID == "LONEP" ).SingleOrDefault();
cust.ContactName = "Neo Anderson" ;
cust.CompanyName = "Lonesome & Pine Restaurant" ;
db.SubmitChanges();
tranaction.Complete();
}
* This source code was highlighted with Source Code Highlighter .
このアプローチでは、TransactionScopeコンテキスト内で実行している作業量を常に評価する必要があります。これは、データベースがこの間ずっとロックされるためです。
LINQ to SQLのより詳細な研究のための資料:Joseph Rutz Jr. プロフェッショナル向けC#2008のLINQ統合クエリ言語
UPD :バージョンフィールドの例を追加