SQL Server Compact Edition データベース上の LINQ to SQL での「行が見つからないか、変更されません」例外を解決するにはどうすればよいですか?
-
09-06-2019 - |
質問
LINQからSQL接続(SQL Server Compact Editionに対して)を使用していくつかのプロパティを更新した後、SubmitchangesをDataContextに実行すると、「行われないか変更されていない」があります。 ChangeConflictException。
var ctx = new Data.MobileServerDataDataContext(Common.DatabasePath);
var deviceSessionRecord = ctx.Sessions.First(sess => sess.SessionRecId == args.DeviceSessionId);
deviceSessionRecord.IsActive = false;
deviceSessionRecord.Disconnected = DateTime.Now;
ctx.SubmitChanges();
クエリは次の SQL を生成します。
UPDATE [Sessions]
SET [Is_Active] = @p0, [Disconnected] = @p1
WHERE 0 = 1
-- @p0: Input Boolean (Size = 0; Prec = 0; Scale = 0) [False]
-- @p1: Input DateTime (Size = 0; Prec = 0; Scale = 0) [9/4/2008 5:12:02 PM]
-- Context: SqlProvider(SqlCE) Model: AttributedMetaModel Build: 3.5.21022.8
明らかな問題は、 ここで、0=1, レコードが読み込まれた後、「deviceSessionRecord」内のすべてのプロパティに主キーが正しく含まれていることを確認しました。また、「ChangeConflictException」をキャッチしても、これが失敗した理由に関する追加情報はありません。また、データベース内の 1 つのレコード (更新しようとしているレコード) でこの例外がスローされることも確認しました。
奇妙なのは、コードの別のセクションに非常によく似た update ステートメントがあり、次の SQL を生成し、実際に SQL Server Compact Edition データベースを更新していることです。
UPDATE [Sessions]
SET [Is_Active] = @p4, [Disconnected] = @p5
WHERE ([Session_RecId] = @p0) AND ([App_RecId] = @p1) AND ([Is_Active] = 1) AND ([Established] = @p2) AND ([Disconnected] IS NULL) AND ([Member_Id] IS NULL) AND ([Company_Id] IS NULL) AND ([Site] IS NULL) AND (NOT ([Is_Device] = 1)) AND ([Machine_Name] = @p3)
-- @p0: Input Guid (Size = 0; Prec = 0; Scale = 0) [0fbbee53-cf4c-4643-9045-e0a284ad131b]
-- @p1: Input Guid (Size = 0; Prec = 0; Scale = 0) [7a174954-dd18-406e-833d-8da650207d3d]
-- @p2: Input DateTime (Size = 0; Prec = 0; Scale = 0) [9/4/2008 5:20:50 PM]
-- @p3: Input String (Size = 0; Prec = 0; Scale = 0) [CWMOBILEDEV]
-- @p4: Input Boolean (Size = 0; Prec = 0; Scale = 0) [False]
-- @p5: Input DateTime (Size = 0; Prec = 0; Scale = 0) [9/4/2008 5:20:52 PM]
-- Context: SqlProvider(SqlCE) Model: AttributedMetaModel Build: 3.5.21022.8
データベース スキーマと LINQ クラスを生成する DBML の両方で、適切なプライマリ フィールド値が識別されていることを確認しました。
これはほぼ 2 つの部分からなる質問だと思います。
- なぜ例外がスローされるのでしょうか?
- 生成された SQL の 2 番目のセットを確認したところ、競合を検出するにはすべてのフィールドをチェックするのが良いように思えますが、これはかなり非効率であると思います。これは常にこのように機能するのでしょうか?主キーのみをチェックする設定はありますか?
過去 2 時間これと戦ってきたので、助けていただければ幸いです。
解決
それは厄介ですが、単純です:
O/R デザイナーのすべてのフィールドのデータ型が SQL テーブルのデータ型と一致するかどうかを確認します。null 可能かどうかを再確認してください。 列は、O/R-Designer と SQL の両方で NULL 可能であるか、両方で NULL 可能ではない必要があります。
たとえば、NVARCHAR 列「title」はデータベース内で NULL 可能としてマークされており、値 NULL が含まれています。O/R マッピングで列が NOT NULLable としてマークされていても、LINQ はそれを正常に読み込み、列文字列を null に設定します。
- これで、何かを変更して、submitchanges()を呼び出します。
- LINQは、タイトルが他の誰かによって変更されていないことを確認するために、「[タイトル]がnull」を含むSQLクエリを生成します。
- LINQは、マッピングの[タイトル]のプロパティを検索します。
- LINQ は [title] NOT NULLable を検出します。
- タイトル]は気付かれないので、論理によってそれは決してnullになることはありません!
- したがって、クエリを最適化すると、LINQは「where 0 = 1」に置き換えられます。これは「never」に相当します。
フィールドのデータ型が SQL のデータ型と一致しない場合、またはフィールドが欠落している場合にも、LINQ は SQL データがデータの読み取り後に変更されていないことを確認できないため、同じ現象が発生します。
他のヒント
まず、問題の原因を知ることが役立ちます。グーグルの解決策が役立つはずです。競合に関する詳細 (テーブル、列、古い値、新しい値) を記録して、後で競合を解決するためのより良い解決策を見つけることができます。
public class ChangeConflictExceptionWithDetails : ChangeConflictException
{
public ChangeConflictExceptionWithDetails(ChangeConflictException inner, DataContext context)
: base(inner.Message + " " + GetChangeConflictExceptionDetailString(context))
{
}
/// <summary>
/// Code from following link
/// https://ittecture.wordpress.com/2008/10/17/tip-of-the-day-3/
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
static string GetChangeConflictExceptionDetailString(DataContext context)
{
StringBuilder sb = new StringBuilder();
foreach (ObjectChangeConflict changeConflict in context.ChangeConflicts)
{
System.Data.Linq.Mapping.MetaTable metatable = context.Mapping.GetTable(changeConflict.Object.GetType());
sb.AppendFormat("Table name: {0}", metatable.TableName);
sb.AppendLine();
foreach (MemberChangeConflict col in changeConflict.MemberConflicts)
{
sb.AppendFormat("Column name : {0}", col.Member.Name);
sb.AppendLine();
sb.AppendFormat("Original value : {0}", col.OriginalValue.ToString());
sb.AppendLine();
sb.AppendFormat("Current value : {0}", col.CurrentValue.ToString());
sb.AppendLine();
sb.AppendFormat("Database value : {0}", col.DatabaseValue.ToString());
sb.AppendLine();
sb.AppendLine();
}
}
return sb.ToString();
}
}
sumbitChanges をラップするためのヘルパーを作成します。
public static class DataContextExtensions
{
public static void SubmitChangesWithDetailException(this DataContext dataContext)
{
try
{
dataContext.SubmitChanges();
}
catch (ChangeConflictException ex)
{
throw new ChangeConflictExceptionWithDetails(ex, dataContext);
}
}
}
次に、変更の送信コードを呼び出します。
Datamodel.SubmitChangesWithDetailException();
最後に、グローバル例外ハンドラーに例外を記録します。
protected void Application_Error(object sender, EventArgs e)
{
Exception ex = Server.GetLastError();
//TODO
}
DataContext にはメソッドがあります。 リフレッシュ ここで役立つかもしれません。これにより、変更が送信される前にデータベース レコードを再ロードでき、保持する値を決定するためのさまざまなモードが提供されます。「KeepChanges」は、私の目的にとっては最も賢明だと思われます。これは、私の変更を、その間にデータベース内で発生した競合しない変更とマージすることを目的としています。
正しく理解していれば。:)
このエラーは、サーバー エクスプローラーからデザイナーにテーブルを再ドラッグして再構築することで解決しました。
これは、複数の DbContext を使用することによって発生する可能性もあります。
たとえば、次のようになります。
protected async Task loginUser(string username)
{
using(var db = new Db())
{
var user = await db.Users
.SingleAsync(u => u.Username == username);
user.LastLogin = DateTime.UtcNow;
await db.SaveChangesAsync();
}
}
protected async Task doSomething(object obj)
{
string username = "joe";
using(var db = new Db())
{
var user = await db.Users
.SingleAsync(u => u.Username == username);
if (DateTime.UtcNow - user.LastLogin >
new TimeSpan(0, 30, 0)
)
loginUser(username);
user.Something = obj;
await db.SaveChangesAsync();
}
}
このコードは、ユーザーが両方のコンテキストで使用され、一方のコンテキストで変更および保存され、もう一方のコンテキストで保存されるため、予測不能な方法で失敗することがあります。「何か」を所有するユーザーのメモリ内表現がデータベース内の内容と一致しないため、この潜在的なバグが発生します。
これを防ぐ 1 つの方法は、ライブラリ メソッドとして呼び出されるコードを、オプションの DbContext を受け取るように記述することです。
protected async Task loginUser(string username, Db _db = null)
{
await EFHelper.Using(_db, async db =>
{
var user = await db.Users...
... // Rest of loginUser code goes here
});
}
public class EFHelper
{
public static async Task Using<T>(T db, Func<T, Task> action)
where T : DbContext, new()
{
if (db == null)
{
using (db = new T())
{
await action(db);
}
}
else
{
await action(db);
}
}
}
したがって、メソッドはオプションのデータベースを受け取り、データベースがない場合は、それ自体でデータベースを作成します。存在する場合は、渡されたものを再利用するだけです。ヘルパー メソッドを使用すると、アプリ全体でこのパターンを簡単に再利用できます。
あなたの質問に対して満足のいく答えが見つかったかどうかはわかりませんが、私も同様の質問を投稿し、最終的には自分で答えました。NOCOUNT のデフォルト接続オプションがデータベースに対してオンになっており、これにより Linq to Sql で行われるすべての更新で ChangeConflictException が発生することが判明しました。私の投稿を参照してください。 ここ.
追加してこれを修正しました (UpdateCheck = UpdateCheck.Never)
みんなに [Column]
定義。
ただし、適切な解決策とは思えません。私の場合、このテーブルには行が削除される別のテーブルとの関連付けがあるという事実に関連しているようです。
これは Windows Phone 7.5 上です。
C# コードでこのエラーをオーバーライドするには、次のものが必要です。
try
{
_db.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e)
{
foreach (ObjectChangeConflict occ in _db.ChangeConflicts)
{
occ.Resolve(RefreshMode.KeepChanges);
}
}
この質問はずっと前に答えられていることは知っていますが、ここで私はここ数時間を壁に頭を打ちつけて過ごしてきたので、このスレッドのどの項目とも関係がないことが判明した解決策を共有したいと思いました。
キャッシング!
データ オブジェクトの select() 部分はキャッシュを使用していました。オブジェクトを更新しようとすると、「行が見つからないか変更されました」エラーが発生しました。
回答のいくつかは、異なる DataContext の使用について言及しており、振り返ってみると、おそらくこれが起こっていたことですが、すぐにキャッシュについて考えるようになったわけではないので、これが誰かの助けになることを願っています。
最近このエラーに遭遇しましたが、問題はデータ コンテキストではなく、コンテキストで Commit が呼び出された後にトリガー内で実行される update ステートメントにあることがわかりました。トリガーが null 非許容フィールドを null 値で更新しようとしたため、コンテキストで上記のメッセージが表示されてエラーが発生していました。
このエラーに対処し、上記の回答で解決策が見つからない他の人を助けるために、この回答を追加しています。
2 つの異なるコンテキストを使用したことでもこのエラーが発生しました。私は単一のデータ コンテキストを使用することでこの問題を解決しました。
私の場合、問題はサーバー全体のユーザー オプションにありました。続く:
https://msdn.microsoft.com/en-us/library/ms190763.aspx
パフォーマンス上の利点を期待して、NOCOUNT オプションを有効にしました。
EXEC sys.sp_configure 'user options', 512;
RECONFIGURE;
これにより、影響を受ける行に対する Linq のチェックが破られることがわかります (.NET ソースから理解できる範囲では)。 ChangeConflictException
オプションをリセットして 512 ビットを除外すると、問題が解決されました。
qub1n の回答を採用した後、私にとっての問題は、データベース列を誤って 10 進数 (18,0) と宣言したことであることがわかりました。10 進数値を割り当てていましたが、データベースによって小数値が変更され、小数部分が削除されました。これにより、行が変更される問題が発生しました。
他の誰かが同様の問題に遭遇した場合にこれを追加するだけです。