艾伟_转载:使用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源代码
我们使用Reflector的FileDisassembler插件,将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.PersistentDataMemberswhere p.IsDeferred && !p.IsAssociationselect 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适合小型系统的开发,但小型不意味着不分层啊。有没有什么办法避免这么多的异常发生呢?
本文其实已经给出了一些线索,在本系列的下一篇随笔中,我将尝试着提供几种解决方案供大家选择。
相关文章
- 使用LINQ to SQL更新数据库(上):问题重重
- 使用LINQ to SQL更新数据库(中):几种解决方案
艾伟_转载:使用LINQ to SQL更新数据库(上):问题重重相关推荐
- 艾伟_转载:使用LINQ to SQL更新数据库(中):几种解决方案
在前一篇文章中,我提出了在使用LINQ to SQL进行更新操作时可能会遇到的几种问题.其实这并不是我一个人遇到的问题,当我在互联网上寻找答案时,我发现很多人都对这个话题发表过类似文章.但另我无法满足 ...
- 转载:LINQ to SQL更新数据库操作
翻译整理ScottGu的关于LINQ to SQL的Part 4: Updating our Database .该Post讲解了如何使用LINQ to SQL更新数据库,以及如何整合业务逻辑和自定义 ...
- 艾伟_转载:简单的自动更新程序实现
本文将演示一种桌面程序自动更新方案,其步骤比较多,但原理非常简单,通用性尚可,对于小型应用来说,直接拿去就可以用了. 原理 服务器端的结构是这样的: 其工作原理如下: Update.asmx仅提供一个 ...
- Linq to Sql: 集成数据库语言查询之一
Linq to Sql: 集成数据库语言查询之一 2007-09-11 11:30:28 来源:天极yesky 作者:随风流月 带您探索"CRUD "操作-创建,接收,更新与删除, ...
- 艾伟_转载:ASP.NET MVC数据验证
关于ASP.NET MVC的验证,用起来很特别,因为MS的封装,使人理解起来很费解.也可能很多人都在Scott Guthrie等人写的一本<ASP.NET MVC 1.0>书中,见过Ner ...
- 艾伟_转载:扩展方法 之 基本数据篇
前一篇我列举了几个最常用到的基于Asp.Net的扩展方法,而这一篇基于基本数据的扩展方法理应不会逊一筹,因为它不局限于Asp.Net.何谓基本数据,这里直接摆定义: C# 中有两种基本数据类型:值类型 ...
- 艾伟_转载:我对NHibernate的感受(1):对延迟加载方式的误解
NHibernate是.NET平台上最著名的ORM框架,虽说出身于Java平台上的Hibernate,但是从外部看来这几乎就是一个.NET平台上的原生产品:有自己的社区,有自己的用户,有自己的商业支持 ...
- mysql 构造 linq语句_[转]查看LINQ生成SQL语句的几种方法
记录LINQ生成的SQL语句是常用的调试方式,而且能根据需要来优化LINQ生成的SQL语句,更能了深入的了解LINQ. DataContext的Log属性来将LINQ to SQL生成的SQL语句格式 ...
- mvc mysql linq_MVC3+Linq to sql 显示数据库中数据表的数据
1:首先创建asp.net mvc3应用程序 2:创建项目完成后 找到controllers文件鼠标右击选择添加控制器 3 为models文件夹添加一个linq to sql类文件,然后把数据库中的数 ...
最新文章
- LSB图像信息隐藏算法matlab,实验二LSB信息隐藏实验.doc
- python3.6安装教程-Python 3.6.6安装教程(附安装包) | 我爱分享网
- 第四天2017/03/31(下午2:结构体、数组)
- 7.2 极大似然估计
- OpenGL:纹理Textures
- Python basestring函数- Python零基础入门教程
- 甚至有些还掉到书本上
- 请大家帮忙,帮我看一下.net的这个问题
- 论文笔记:微表情识别综述1
- SolidWorks 2010 SP0.0 最新下载+序列号 注册机及方法
- linux命令-文件命令
- 通过命令行操作iOS模拟器
- Java 已知三边求三角形求面积
- python国际象棋ai程序_Python开发AI应用-国际象棋应用
- RabbitMQ高可用集群搭建
- MYSQL(连接查询)
- Java格式化json格式文本数据
- dreamweaver网页设计作业制作 (NBA篮球网页设计与制作) HTML+CSS
- 服务器返回的my为空,WCF REST服务:方法参数(对象)为空
- IE加载OCX插件崩溃原因之栈溢出问题