关于.net:如何解决LINQ to SQL Server Compact Edition数据库上的SQL中的“找不到或更改行”异常?

关于.net:如何解决LINQ to SQL Server Compact Edition数据库上的SQL中的“找不到或更改行”异常?

What can I do to resolve a “Row not found or changed” Exception in LINQ to SQL on a SQL Server Compact Edition Database?

在使用LINQ to SQL连接更新了几个属性(针对SQL Server Compact Edition)后,对DataContext执行SubmitChanges时,出现"未找到或更改行"。 ChangeConflictException。

1
2
3
4
5
6
7
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:

1
2
3
4
5
6
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"时,没有有关此失败原因的其他信息。我还确认了该异常与数据库中的一条记录(我正在尝试更新的记录)一起引发

奇怪的是,我在代码的不同部分中有一个非常相似的update语句,它生成以下SQL,并且确实更新了SQL Server Compact Edition数据库。

1
2
3
4
5
6
7
8
9
10
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中都已经标识了正确的主字段值。

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

  • 为什么会引发异常?
  • 在检查了第二组生成的SQL之后,似乎为了检测冲突,最好检查所有字段,但是我认为这样做效率不高。这是始终有效的方式吗?是否有仅检查主键的设置?
  • 在过去的两个小时中,我一直在与这个问题作斗争,因此我们将不胜感激。


    那很讨厌,但是很简单:

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

    例如,NVARCHAR列" title"在您的数据库中被标记为NULLable,并且包含值NULL。即使在O / R映射中将该列标记为"不可为空",LINQ也会成功加载它并将column-String设置为null。

    • 现在,您进行更改并致电
      SubmitChanges()。
    • LINQ将生成一个SQL查询
      包含" WHERE [title] IS NULL",以确保标题未被其他人更改。
    • LINQ查找属性
      映射中的[title]。
    • LINQ将发现[title]不可为空。
    • 由于[title]不可为空,因此
      逻辑上永远不可能为NULL!
    • 因此,优化查询LINQ
      将其替换为" where 0 = 1",
      SQL等同于"从不"。

    当字段的数据类型与SQL中的数据类型不匹配时,或者如果缺少字段,则会出现相同的症状,因为LINQ无法确保自读取数据以来SQL数据没有更改。


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    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的帮助器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public static class DataContextExtensions
    {
        public static void SubmitChangesWithDetailException(this DataContext dataContext)
        {  
            try
            {        
                dataContext.SubmitChanges();
            }
            catch (ChangeConflictException ex)
            {
                throw new ChangeConflictExceptionWithDetails(ex, dataContext);
            }          
        }
    }

    然后调用提交更改代码:

    1
    Datamodel.SubmitChangesWithDetailException();

    最后,将异常记录在全局异常处理程序中:

    1
    2
    3
    4
    5
    protected void Application_Error(object sender, EventArgs e)
    {        
        Exception ex = Server.GetLastError();
        //TODO
    }


    在DataContext上有一个称为Refresh的方法,可能在这里有所帮助。它使您可以在提交更改之前重新加载数据库记录,并提供不同的模式来确定要保留的值。就我而言," KeepChanges"似乎是最聪明的,它旨在将我的更改与同时发生在数据库中的任何无冲突的更改合并。

    如果我正确理解。 :)


    这也可能是由于使用多个DbContext引起的。

    因此,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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);
            }
        }
    }

    因此,现在您的方法采用一个可选数据库,如果没有,则自行创建一个数据库。如果存在,则仅重用传入的内容。helper方法使在整个应用程序中重用此模式变得容易。


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


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
                try
                {
                    _db.SubmitChanges(ConflictMode.ContinueOnConflict);
                }
                catch (ChangeConflictException e)
                {
                    foreach (ObjectChangeConflict occ in _db.ChangeConflicts)
                    {
                        occ.Resolve(RefreshMode.KeepChanges);
                    }
                }


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

    不过,这感觉并不适合。就我而言,这似乎与以下事实有关:该表与从中删除行的另一个表相关联。

    这是在Windows Phone 7.5上。


    我不知道您是否对您的问题找到满意的答案,但我也发布了类似的问题,并最终自己回答了。事实证明,数据库的NOCOUNT默认连接选项已打开,这对Linq对Sql所做的每次更新都会导致ChangeConflictException。您可以在这里参考我的帖子。


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

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


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

    https://msdn.microsoft.com/zh-CN/library/ms190763.aspx

    我启用了NOCOUNT选项,希望获得一些性能上的好处:

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

    事实证明,这打破了Linq对"受影响的行"的检查(据我从.NET来源了解的最多),导致ChangeConflictException

    重置选项以排除512位可解决此问题。


    由于使用了两个不同的上下文,所以我也收到了此错误。我通过使用单个数据上下文解决了此问题。


    我最近遇到了此错误,发现问题不在于我的数据上下文,而是在上下文上调用Commit之后在触发器内触发了更新语句。
    触发器正在尝试使用空值更新不可为空的字段,并导致上下文因上述消息而出错。

    我添加此答案仅是为了帮助其他人处理此错误,而没有在上面的答案中找到解决方案。


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

    正在缓存!

    我的数据对象的select()部分正在使用缓存。在更新对象时,出现了"找不到行或更改行"错误。

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


    推荐阅读