艾伟_转载:使用LINQ to SQL更新数据库(上):问题重重

在学习LINQ时,我几乎被一个困难所击倒,这就是你从标题中看到的更新数据库的操作。下面我就一步步带你走入这泥潭,请准备好砖头和口水,Follow me。

从最简单的情况入手

我们以Northwind数据库为例,当需要修改一个产品的ProductName时,可以在客户端直接写下这样的代码:

// List 0NorthwindDataContext db = new NorthwindDataContext();
Product product = db.Products.Single(p => p.ProductID == 1);
product.ProductName = "Chai Changed";
db.SubmitChanges();

测试一下,更新成功。不过我相信,在各位的项目中不会出现这样的代码,因为它简直没法复用。好吧,让我们对其进行重构,提取至一个方法中。参数应该是什么呢?是新的产品名称,以及待更新的产品ID。嗯,好像是这样的。

public void UpdateProduct(int id, string productName)
{
    NorthwindDataContext db = new NorthwindDataContext();
    Product product = db.Products.Single(p => p.ProductID == id);
    product.ProductName = productName;
    db.SubmitChanges();
}

在实际的项目中,我们不可能仅仅只修改产品名称。Product的其他字段同样也是修改的对象。那么UpdateProduct方法的签名将变成如下的形式:

public void UpdateProduct(int id,
    string productName,
    int suplierId,
    int categoryId,
    string quantityPerUnit,
    decimal unitPrice,
    short unitsInStock,
    short unitsOnOrder,
    short reorderLevel)

当然这只是简单的数据库,在实际项目中,二十、三十甚至上百个字段的情况也不少见。谁能忍受这样的方法呢?这样写,还要Product对象干什么呢?

对啊,把Product作为方法的参数,把恼人的赋值操作抛给客户代码吧。同时,我们将获取Product实例的代码提取出来,形成GetProduct方法,并且将与数据库操作相关的方法放到一个专门负责和数据库打交道的ProductRepository类中。哦耶,SRP!

// List 1
// ProductRepository
public Product GetProduct(int id)
{
    NorthwindDataContext db = new NorthwindDataContext();
    return db.Products.SingleOrDefault(p => p.id == id);
}
public void UpdateProduct(Product product)
{
    NorthwindDataContext db = new NorthwindDataContext();
    db.Products.Attach(product);
    db.SubmitChanges();
}
// Client code
ProductRepository repository = new ProductRepository();
Product product = repository.GetProduct(1);
product.ProductName = "Chai Changed";
repository.UpdateProduct(product);

在这里我使用了Attach方法,将Product的一个实例附加到其他的DataContext上。对于默认的Northwind数据库来说,这样做的结果就是得到下面的异常:

// Exception 1 NotSupportException:
已尝试Attach或Add实体,该实体不是新实体,可能是从其他DataContext中加载来的。不支持这种操作。
An attempt has been made to Attach or Add an entity that is not new,
perhaps having been loaded from another DataContext. This is not supported

查看MSDN我们知道,在将实体序列化到客户端时,这些实体会与其原始DataContext分离。DataContext不再跟踪这些实体的更改或它们与其他对象的关联。这时如果要更新或者删除数据,则必须在调用SubmitChanges之前使用Attach方法将实体附加到新的DataContext中,否则就会抛出上面的异常。

而在Northwind数据库中,Product类包含三个与之相关的类(即外键关联):Order_Detail、Category和Supllier。在上面的例子中,我们虽然把Product进行了Attach,但却没有Attach与其相关联的类,因此抛出NotSupportException。

那么如何关联与Product相关的类呢?这看上去似乎十分复杂,即便简单地如Northwind这样的数据库亦是如此。我们似乎必须先获取与原始Product相关的Order_Detail、Category和Supllier的原始类,然后再分别Attach到当前的DataContext中,但实际上即使这样做也同样会抛出NotSupportException。

那么究竟该如何实现更新操作呢?为了简便起见,我们删除Northwind.dbml中的其他实体类,只保留Product。这样就可以从最简单的情况开始入手分析了。

问题重重

删除其他类之后,我们再次执行List 1中的代码,然而数据库并没有更改产品的名称。通过查看Attach方法的重载版本,我们很容易发现问题所在。

Attach(entity)方法默认调用Attach(entity, false)重载,它将以未修改的状态附加相应实体。如果Product对象没有被修改,那么我们应该调用该重载版本,将Product对象以未修改的状态附加到DataContext,以便后续操作。而此时的Product对象的状态是“已修改”,我们只能调用Attach(entity, true)方法。

于是我们将List 1的相关代码改为Attach(product, true),看看发生了什么?

// Exception 2 InvalidOperationException:
如果实体声明了版本成员或者没有更新检查策略,则只能将它附加为没有原始状态的已修改实体。
An entity can only be attached as modified without original state
if it declares a version member or does not have an update check policy.

LINQ to SQL使用RowVersion列来实现默认的乐观式并发检查,否则在以修改状态向DataContext附加实体的时候,就会出现上面的错误。实现RowVersion列的方法有两种,一种是为数据库表定义一个timestamp类型的列,另一种方法是在表主键所对应的实体属性上,定义IsVersion=true特性。注意,不能同时拥有TimeStamp列和IsVersion=true特性,否则将抛出InvalidOprationException:成员“System.Data.Linq.Binary TimeStamp”和“Int32 ProductID”都标记为行版本。在本文中,我们使用timestamp列来举例。

为Products表建立名为TimeStamp、类型为timestamp的列之后,将其重新拖拽到设计器中,然后执行List 1中的代码。谢天谢地,终于成功了。

现在,我们再向设计器中拖入Categories表。这次学乖了,先在Categories表中添加timestamp列。测试一下,居然又是Exception 1中的错误!删除Categories的timestamp列,问题依旧。天哪,可怕的Attach方法里究竟干了什么?

哦,对了,Attach方法还有一个重载版本,我们来试一下吧。

public void UpdateProduct(Product product)
{
    NorthwindDataContext db = new NorthwindDataContext();
    Product oldProduct = db.Products.SingleOrDefault(p => p.ProductID == product.ProductID);
    db.Products.Attach(product, oldProduct);
    db.SubmitChanges();
}

还是Exception 1的错误!

我就倒!Attach啊Attach,你究竟怎么了?

探索LINQ to SQL源代码

我们使用ReflectorFileDisassembler插件,将System.Data.Linq.dll反编译成cs代码,并生成项目文件,这有助于我们在Visual Studio中进行查找和定位。

什么时候抛出Exception 1?

我们先从System.Data.Linq.resx中找到Exception 1所描述的信息,得到键“CannotAttachAddNonNewEntities”,然后找到System.Data.Linq.Error.CannotAttachAddNonNewEntities()方法,查找该方法的所有引用,发现在两个地方使用了该方法,分别为StandardChangeTracker.Track方法和InitializeDeferredLoader方法。

我们再打开Table.Attach(entity, bool)的代码,不出所料地发现它调用了StandardChangeTracker.Track方法(Attach(entity, entity)方法中也是如此):

trackedObject = this.context.Services.ChangeTracker.Track(entity, true);

在Track方法中,抛出Exception 1的是下面的代码:

if (trackedObject.HasDeferredLoaders)
{
    throw System.Data.Linq.Error.CannotAttachAddNonNewEntities();
}

于是我们将注意力转移到StandardTrackedObject.HasDeferredLoaders属性上来:

internal override bool HasDeferredLoaders
{
    get
    {
        foreach (MetaAssociation association in this.Type.Associations)
        {
            if (this.HasDeferredLoader(association.ThisMember))
            {
                return true;
            }
        }
        foreach (MetaDataMember member in from p in this.Type.PersistentDataMembers
            where p.IsDeferred && !p.IsAssociation
            select p)
        {
            if (this.HasDeferredLoader(member))
            {
                return true;
            }
        }
        return false;
    }
}

从中我们大致可以推出,只要实体中存在延迟加载的项时,执行Attach操作就会抛出Exception 1。这正好符合我们发生Exception 1的场景——Product类含有延迟加载的项。

那么避免该异常的方法也浮出水面了——移除Product中需要延迟加载的项。如何移除呢?可以使用DataLoadOptions立即加载,也可以将需要延迟加载的项设置为null。但是第一种方法行不通,只好使用第二种方法了。

// List 2
class ProductRepository
{
    public Product GetProduct(int id)
    {
        NorthwindDataContext db = new NorthwindDataContext();
        return db.Products.SingleOrDefault(p => p.ProductID == id);
    }
    public Product GetProductNoDeffered(int id)
    {
        NorthwindDataContext db = new NorthwindDataContext();
        //DataLoadOptions options = new DataLoadOptions();
        //options.LoadWith(p => p.Category);
        //db.LoadOptions = options;
        var product = db.Products.SingleOrDefault(p => p.ProductID == id);
        product.Category = null;
        return product;
    }
    public void UpdateProduct(Product product)
    {
        NorthwindDataContext db = new NorthwindDataContext();
        db.Products.Attach(product, true);
        db.SubmitChanges();
    }
}
// Client code
ProductRepository repository = new ProductRepository();
Product product = repository.GetProductNoDeffered(1);
product.ProductName = "Chai Changed";
repository.UpdateProduct(product);

什么时候抛出Exception 2?

按照上一节的方法,我们很快找到了抛出Exception 2的代码,幸运的是,整个项目中只有这一处:

if (asModified && ((inheritanceType.VersionMember == null) && inheritanceType.HasUpdateCheck))
{
    throw System.Data.Linq.Error.CannotAttachAsModifiedWithoutOriginalState();
}

可以看到,当Attach的第二个参数asModified为true、不包含RowVersion列(VersionMember=null)、且含有更新检查的列(HasUpdateCheck)时,会抛出Exception 2。HasUpdateCheck的代码如下:

public override bool HasUpdateCheck
{
    get
    {
        foreach (MetaDataMember member in this.PersistentDataMembers)
        {
            if (member.UpdateCheck != UpdateCheck.Never)
            {
                return true;
            }
        }
        return false;
    }
}

这也符合我们的场景——Products表没有RowVersion列,并且设计器自动生成的代码中,所有字段的UpdateCheck特性均为默认的Always,即HasUpdateCheck属性为true。

避免Exception 2的方法就更简单了,为所有表都添加TimeStamp列或对所有表的主键字段上设置IsVersion=true字段。由于后一种方法要修改自动生成的类,并随时都会被新的设计所覆盖,因此我建议使用前一种方法。

如何使用Attach方法?

经过上面的分析,我们可以找出与Attach方法相关的两个条件:是否有RowVersion列以及是否存在外键关联(即需要延迟加载的项)。我将这两个条件与Attach的几个重载使用的情况总结出了一个表,在看下面这个表时,你需要做好充分的心理准备。


序号


Attach方法


RowVersion列


是否有关联


描述

1 Attach(entity) 没有修改
2 Attach(entity) NotSupportException: 已尝试Attach或Add实体,该实体不是新实体,可能是从其他DataContext中加载来的。不支持这种操作。
3 Attach(entity) 没有修改
4 Attach(entity) 没有修改。如果子集没有RowVersion列则与2一样。
5 Attach(entity, true) InvalidOperationException:如果实体声明了版本成员或者没有更新检查策略,则只能将它附加为没有原始状态的已修改实体。
6 Attach(entity, true) NotSupportException: 已尝试Attach或Add实体,该实体不是新实体,可能是从其他DataContext中加载来的。不支持这种操作。
7 Attach(entity, true) 正常修改(强制修改RowVersion列会报错)
8 Attach(entity, true) NotSupportException: 已尝试Attach或Add实体,该实体不是新实体,可能是从其他DataContext中加载来的。不支持这种操作。
9 Attach(entity, entity)
DuplicateKeyException:不能添加其键已在使用中的实体。

10 Attach(entity, entity) NotSupportException: 已尝试Attach或Add实体,该实体不是新实体,可能是从其他DataContext中加载来的。不支持这种操作。
11 Attach(entity, entity)
DuplicateKeyException:不能添加其键已在使用中的实体。

12 Attach(entity, entity) NotSupportException: 已尝试Attach或Add实体,该实体不是新实体,可能是从其他DataContext中加载来的。不支持这种操作。

Attach居然只能在第7种情况(包含RowVersion列并且无外键关联)时才能正常更新!而这种情况对于一个基于数据库的系统来说,几乎不可能出现!这是一个什么样的API啊?

总结

让我们平静一下心情,开始总结吧。

如果像List 0那样,直接在UI里写LINQ to SQL代码,则什么不幸的事也不会发生。但是如果要抽象出一个单独的数据访问层,灾难就会降临。这是否说明LINQ to SQL不适合多层架构的开发?很多人都说LINQ to SQL适合小型系统的开发,但小型不意味着不分层啊。有没有什么办法避免这么多的异常发生呢?

本文其实已经给出了一些线索,在本系列的下一篇随笔中,我将尝试着提供几种解决方案供大家选择。

相关文章

时间: 2024-09-17 15:19:37

艾伟_转载:使用LINQ to SQL更新数据库(上):问题重重的相关文章

艾伟_转载:LINQ to SQL、NHibernate比较(二)-- LINQ to SQL实例

    用ADO.NET操作数据库大家一定再熟悉不过了,select.insert.update等等SQL语句大家也都必然滚瓜烂熟.我将自己在学习LINQ to SQL过程中的动手经历记录下来,作为今后学习的参考,也希望对刚刚接触的人有一点帮助.     我在本文涉及到一个很简单的系统,利用DataGridView实现数据库数据的批量增.删.改,不是什么强大的功能.     如果有人感兴趣,可以在看完我的这篇文章之后用ADO.NET实现同样的功能,看看到底会比使用LINQ to SQL多多少时间

艾伟_转载:VS2008连接SQL Server数据库文件出错的解决方案

有园子里的朋友问到一个问题:系统上安装的是vs2008+Sql 2005 developer(没有安装Sql server 2005 Express ) 用代码直接连SQL Server服务器没有问题,但在项目中数据源改用SQL Server数据库文件时出错,提示错误如下:"与SQL Server文件(*.mdf)的连接要求安装SQL Server2005才能正常工作,请确认是否安装了该组件,--" 英文版为"Connections to SQL Server Files (

艾伟_转载:使用LINQ to SQL更新数据库(中):几种解决方案

在前一篇文章中,我提出了在使用LINQ to SQL进行更新操作时可能会遇到的几种问题.其实这并不是我一个人遇到的问题,当我在互联网上寻找答案时,我发现很多人都对这个话题发表过类似文章.但另我无法满足的是,他们尽管提出了问题,却没有进行详细的剖析,只给出了解决方案(如添加RowVersion列.去除关联等),但却没有说明为什么必须这么做.这也是我写上篇的初衷,希望通过对LINQ to SQL源代码的分析,来一步一步找出解决问题的办法.本文将对这些方法一一进行讨论. 方案一:重新赋值 在Terry

一起谈.NET技术,使用LINQ to SQL更新数据库(上):问题重重

在学习LINQ时,我几乎被一个困难所击倒,这就是你从标题中看到的更新数据库的操作.下面我就一步步带你走入这泥潭,请准备好砖头和口水,Follow me. 从最简单的情况入手 我们以Northwind数据库为例,当需要修改一个产品的ProductName时,可以在客户端直接写下这样的代码: // List 0NorthwindDataContext db = new NorthwindDataContext(); Product product = db.Products.Single(p =>

一起谈.NET技术,使用LINQ to SQL更新数据库(中):几种解决方案

在前一篇文章中,我提出了在使用LINQ to SQL进行更新操作时可能会遇到的几种问题.其实这并不是我一个人遇到的问题,当我在互联网上寻找答案时,我发现很多人都对这个话题发表过类似文章.但另我无法满足的是,他们尽管提出了问题,却没有进行详细的剖析,只给出了解决方案(如添加RowVersion列.去除关联等),但却没有说明为什么必须这么做.这也是我写上篇的初衷,希望通过对LINQ to SQL源代码的分析,来一步一步找出解决问题的办法.本文将对这些方法一一进行讨论. 方案一:重新赋值 在Terry

使用LINQ to SQL更新数据库(下):性能测试

在上一篇随笔中,我们列举了使用LINQ to SQL对数据库进行更新的5中方案.本文将对这几种方案进 行测试和对比,力求找出一个最佳实践. 准备工作 我们的测试还是基于Products表.为了使测试更符合实际,我们将与之关联的Categories.Suplliers 和Order_Details表都添加进来.首先创建一个IProductRepository接口,定义插入.查找.更新操作: public interface IProductRepository { void InsertProdu

使用LINQ to SQL更新数据库(中):几种解决方案

在前一篇文章中,我提出了在使用LINQ to SQL进行更新操作时可能会遇到的几种问题.其实这并不是 我一个人遇到的问题,当我在互联网上寻找答案时,我发现很多人都对这个话题发表过类似文章.但另我 无法满足的是,他们尽管提出了问题,却没有进行详细的剖析,只给出了解决方案(如添加RowVersion列 .去除关联等),但却没有说明为什么必须这么做.这也是我写上篇的初衷,希望通过对LINQ to SQL源 代码的分析,来一步一步找出解决问题的办法.本文将对这些方法一一进行讨论. 方案一:重新赋值 在T

使用LINQ to SQL更新数据库(上):问题重重

在学习LINQ时,我几乎被一个困难所击倒,这就是你从标题中看到的更新数据库的操作.下面我就一 步步带你走入这泥潭,请准备好砖头和口水,Follow me. 从最简单的情况入手 我们以Northwind数据库为例,当需要修改一个产品的ProductName时,可以在客户端直接写下这样的 代码: // List 0 NorthwindDataContext db = new NorthwindDataContext(); Product product = db.Products.Single(p

艾伟_转载:简单的自动更新程序实现

本文将演示一种桌面程序自动更新方案,其步骤比较多,但原理非常简单,通用性尚可,对于小型应用来说,直接拿去就可以用了. 原理 服务器端的结构是这样的: 其工作原理如下: Update.asmx仅提供一个功能,就是检测是否需要更新,在需要更新的时候就返回一个更新地址,通常情况下返回的地址就是Download.ashx,而在某些特殊情况下,也可以修改服务端使之从其他Url提供更新下载.检测是否需要更新的的具体做法是:首先获取Updata目录中的主程序版本号,再获取数据库中的最新版本号,两者对比.如果相