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

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

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

从最简单的情况入手

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

// List 0
NorthwindDataContext
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适合小型系统的开发,但小型不意味着不分层啊。有没有什么办法避免这么多的异常发生呢?

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

相关文章

目录
相关文章
|
4天前
|
SQL Oracle 关系型数据库
sql语句创建数据库
在创建数据库之前,请确保你有足够的权限,并且已经考虑了数据库的安全性和性能需求。此外,不同的DBMS可能有特定的最佳实践和配置要求,因此建议查阅相关DBMS的官方文档以获取更详细和准确的信息。
|
2天前
|
SQL Java 数据库连接
Java从入门到精通:2.3.2数据库编程——了解SQL语言,编写基本查询语句
Java从入门到精通:2.3.2数据库编程——了解SQL语言,编写基本查询语句
|
4天前
|
SQL 缓存 数据库
sql 数据库优化
SQL数据库优化是一个复杂且关键的过程,涉及多个层面的技术和策略。以下是一些主要的优化建议: 查询语句优化: 避免全表扫描:在查询时,尽量使用索引来减少全表扫描,提高查询速度。 使用合适的子查询方式:子查询可能降低查询效率,但可以通过优化子查询的结构或使用连接(JOIN)替代子查询来提高性能。 简化查询语句:避免不必要的复杂查询,尽量使SQL语句简单明了。 使用EXISTS替代IN:在查询数据是否存在时,使用EXISTS通常比IN更快。 索引优化: 建立合适的索引:对于经常查询的列,如主键和外键,应创建相应的索引。同时,考虑使用覆盖索引来进一步提高性能。 避免过多的索引:虽然索引可以提高查询
|
4天前
|
SQL XML 数据库
sql导入数据库命令
在SQL Server中,数据库导入可通过多种方式实现:1) 使用SSMS的“导入数据”向导从各种源(如Excel、CSV)导入;2) BULK INSERT语句适用于导入文本文件;3) bcp命令行工具进行批量数据交换;4) OPENROWSET函数直接从外部数据源(如Excel)插入数据。在操作前,请记得备份数据库,并可能需对数据进行预处理以符合SQL Server要求。注意不同方法可能依版本和配置而异。
|
11天前
|
SQL 数据库
数据库SQL语言实战(二)
数据库SQL语言实战(二)
|
12天前
|
SQL 关系型数据库 数据库
【后端面经】【数据库与MySQL】SQL优化:如何发现SQL中的问题?
【4月更文挑战第12天】数据库优化涉及硬件升级、操作系统调整、服务器/引擎优化和SQL优化。SQL优化目标是减少磁盘IO和内存/CPU消耗。`EXPLAIN`命令用于检查SQL执行计划,关注`type`、`possible_keys`、`key`、`rows`和`filtered`字段。设计索引时考虑外键、频繁出现在`where`、`order by`和关联查询中的列,以及区分度高的列。大数据表改结构需谨慎,可能需要停机、低峰期变更或新建表。面试中应准备SQL优化案例,如覆盖索引、优化`order by`、`count`和索引提示。优化分页查询时避免大偏移量,可利用上一批的最大ID进行限制。
38 3
|
5月前
|
SQL Oracle 关系型数据库
本机不安装Oracle客户端,使用PL/SQL Developer连接远程数据库
本机不安装Oracle客户端,使用PL/SQL Developer连接远程数据库
142 0
|
SQL 程序员 数据库
【python】连接sql server数据库,并实现简单的增删改查(1)
Python编程语言越来越受到大家的喜爱,本篇文章就从链接微软数据库进行增删改查操作的讲解
526 0
|
SQL 数据库连接 数据库
Qt实用技巧:Qt连接SQL Server数据库(需要配置ODBC)
Qt实用技巧:Qt连接SQL Server数据库(需要配置ODBC)
|
3月前
|
SQL 关系型数据库 数据库连接
Python 连接 SQL 数据库 -pyodbc
以下是如何在 Python 中使用 pyodbc 连接到 SQL 数据库的基本步骤和详解
52 0