100

在使用 LINQ to SQL 连接(针对 SQL Server Compact Edition)更新几个属性后对 DataContext 执行 SubmitChanges 时,我收到“未找到或更改行”。更改冲突异常。

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

明显的问题是WHERE 0=1,加载记录后,我已经确认“deviceSessionRecord”中的所有属性都正确包含主键。此外,当捕获“ChangeConflictException”时,没有关于失败原因的其他信息。我还确认,数据库中只有一条记录(我正在尝试更新的记录)引发了此异常

奇怪的是,我在不同的代码部分有一个非常相似的更新语句,它生成以下 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 中都确定了正确的主字段值。

我想这几乎是一个两部分的问题:

  1. 为什么会抛出异常?
  2. 在查看了第二组生成的 SQL 之后,似乎为了检测冲突,检查所有字段会很好,但我想这将是相当低效的。这是总是这样吗?有没有只检查主键的设置?

在过去的两个小时里,我一直在与此作斗争,因此我们将不胜感激。

4

15 回答 15

195

这很讨厌,但很简单:

检查 O/R-Designer 中所有字段的数据类型是否与 SQL 表中的数据类型匹配。 仔细检查是否可以为空!列应该在 O/R-Designer 和 SQL 中都可以为空,或者在两者中都不能为空。

例如,一个 NVARCHAR 列“title”在您的数据库中被标记为 NULLable,并且包含值 NULL。即使该列在 O/R-Mapping 中被标记为 NOT NULLable,LINQ 也会成功加载它并将 column-String 设置为 null。

  • 现在你改变一些东西并调用 SubmitChanges()。
  • LINQ 将生成一个包含“WHERE [title] IS NULL”的 SQL 查询,以确保标题没有被其他人更改。
  • LINQ 在映射中查找 [title] 的属性。
  • LINQ 会发现 [title] NOT NULLable。
  • 由于 [title] 不可为 NULL,因此从逻辑上讲,它永远不可能为 NULL!
  • 因此,优化查询时,LINQ 将其替换为“where 0 = 1”,即“never”的 SQL 等效项。

当字段的数据类型与 SQL 中的数据类型不匹配时,或者如果字段丢失,也会出现相同的症状,因为 LINQ 将无法确保 SQL 数据在读取数据后没有更改。

于 2008-09-17T14:44:57.607 回答
26

首先,了解导致问题的原因很有用。谷歌搜索解决方案应该会有所帮助,您可以记录有关冲突的详细信息(表、列、旧值、新值),以便为以后解决冲突找到更好的解决方案:

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
}
于 2015-08-28T21:24:58.630 回答
17

DataContext 上有一个名为Refresh的方法,在这里可能会有所帮助。它允许您在提交更改之前重新加载数据库记录,并提供不同的模式来确定要保留哪些值。“KeepChanges”对我来说似乎是最聪明的,它旨在将我的更改与同时在数据库中发生的任何非冲突更改合并。

如果我理解正确的话。:)

于 2010-09-03T04:05:31.337 回答
11

这也可能是由使用多个 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();
    }
}

这段代码会时不时地以不可预测的方式失败,因为用户在两种上下文中都使用过,在一个上下文中更改并保存,然后在另一个上下文中保存。拥有“某物”的用户的内存表示与数据库中的内容不匹配,因此您会遇到这个潜伏的错误。

防止这种情况的一种方法是编写任何可能作为库方法调用的代码,使其采用可选的 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);
        }
    }
}

所以现在你的方法需要一个可选的数据库,如果没有,就自己创建一个。如果有,它只是重用传入的内容。辅助方法可以很容易地在你的应用程序中重用这个模式。

于 2011-05-23T19:31:38.993 回答
10

我通过将表格从服务器资源管理器重新拖到设计器并重新构建来解决了这个错误。

于 2009-02-04T19:47:01.713 回答
4

这是您需要在 C# 代码上覆盖此错误的内容:

            try
            {
                _db.SubmitChanges(ConflictMode.ContinueOnConflict);
            }
            catch (ChangeConflictException e)
            {
                foreach (ObjectChangeConflict occ in _db.ChangeConflicts)
                {
                    occ.Resolve(RefreshMode.KeepChanges);
                }
            }
于 2016-08-18T15:07:46.013 回答
3

我不知道您是否对您的问题找到了满意的答案,但我发布了一个类似的问题并最终自己回答了。原来是为数据库打开了 NOCOUNT 默认连接选项,这导致每次使用 Linq to Sql 进行的更新都会引发 ChangeConflictException。你可以在这里参考我的帖子。

于 2009-03-19T17:22:23.177 回答
3

我通过添加(UpdateCheck = UpdateCheck.Never)到所有[Column]定义来解决这个问题。

不过,感觉这不是一个合适的解决方案。在我的情况下,这似乎与该表与另一个表相关联,从该表中删除了一行。

这是在 Windows Phone 7.5 上。

于 2012-07-03T10:27:21.013 回答
1

在我的例子中,当两个具有不同 LINQ-to-SQL 数据上下文的用户以相同的方式更新同一个实体时,就会引发错误。当第二个用户尝试更新时,他们在数据上下文中的副本已经过时,即使它是在第一次更新完成后读取的。

我在 Akshay Phadke 的这篇文章中发现了解释和解决方案: https ://www.c-sharpcorner.com/article/overview-of-concurrency-in-linq-to-sql/

这是我最常用的代码:

try
{
    this.DC.SubmitChanges();
}
catch (ChangeConflictException)
{
     this.DC.ChangeConflicts.ResolveAll(RefreshMode.OverwriteCurrentValues);

     foreach (ObjectChangeConflict objectChangeConflict in this.DC.ChangeConflicts)
     {
         foreach (MemberChangeConflict memberChangeConflict in objectChangeConflict.MemberConflicts)
         {
             Debug.WriteLine("Property Name = " + memberChangeConflict.Member.Name);
             Debug.WriteLine("Current Value = " + memberChangeConflict.CurrentValue.ToString());
             Debug.WriteLine("Original Value = " + memberChangeConflict.OriginalValue.ToString());
             Debug.WriteLine("Database Value = " + memberChangeConflict.DatabaseValue.ToString());
         }
     }
     this.DC.SubmitChanges();
     this.DC.Refresh(RefreshMode.OverwriteCurrentValues, att);
 }

当我在调试时查看输出窗口时,我可以看到当前值与数据库值匹配。“原始价值”始终是罪魁祸首。这是应用更新之前数据上下文读取的值。

感谢 MarceloBarbosa 的灵感。

于 2020-06-18T21:43:05.620 回答
0

我知道这个问题早就被回答了,但是在这里我花了最后几个小时把头撞在墙上,我只是想分享我的解决方案,结果证明与这个线程中的任何项目都不相关:

缓存!

我的数据对象的 select() 部分正在使用缓存。在更新对象时,出现了 Row Not Found or Changed 错误。

有几个答案确实提到了使用不同的 DataContext,回想起来这可能是正在发生的事情,但它并没有立即让我想到缓存,所以希望这会对某人有所帮助!

于 2011-09-06T15:26:21.000 回答
0

我最近遇到了这个错误,发现问题不在于我的数据上下文,而是在上下文中调用 Commit 后触发器内部触发了更新语句。触发器试图用 null 值更新不可为 null 的字段,并导致上下文出错并显示上述消息。

我添加这个答案只是为了帮助其他人处理这个错误,而不是在上面的答案中找到解决方案。

于 2011-11-25T17:20:28.887 回答
0

由于使用了两种不同的上下文,我也遇到了这个错误。我通过使用单一数据上下文解决了这个问题。

于 2015-06-25T09:50:19.170 回答
0

就我而言,问题出在服务器范围的用户选项上。下列的:

https://msdn.microsoft.com/en-us/library/ms190763.aspx

我启用了 NOCOUNT 选项,希望获得一些性能优势:

EXEC sys.sp_configure 'user options', 512;
RECONFIGURE;

事实证明,这破坏了 Linq 对受影响行的检查(我可以从 .NET 源代码中弄清楚),导致ChangeConflictException

重置选项以排除 512 位解决了该问题。

于 2015-10-26T11:46:18.923 回答
0

使用 qub1n 的回答后,我发现问题在于我无意中将数据库列声明为十进制(18,0)。我正在分配一个十进制值,但数据库正在更改它,剥离小数部分。这导致了行更改问题。

如果其他人遇到类似问题,只需添加此内容。

于 2016-10-03T18:17:35.457 回答
0

我知道这是一篇较旧的帖子,但这个问题今天仍然存在问题。我想分享我的经验;因为我的解决方案与接受的答案略有不同。然而,接受的答案确实让我解决了我的问题,所以谢谢!

在我的例子中,我有一个更新触发器,它会在表中一行的状态发生变化时自动将一行插入到状态历史表中(SQL Server);基于一组已知代码。我的历史表的状态 ID 列有一个 NOT NULL 属性,我的 INSERT 语句没有考虑到以前未知的代码可能会漏掉;从而导致插入的行失败。

所以这个故事的寓意是除了检查您的数据模型之外,请务必查看您定义的任何触发器,因为这也会导致“未找到或更改行”错误。

Hope this helps someone else down the line; thanks all!

于 2021-08-02T14:59:36.820 回答