Pro LINQ 之五:LINQ to Entity

引言

《Pro LINQ in C# 2008》本没有LINQ to Entity的内容,有的只是LINQ to SQL Entity。前者是ADO.NET Entity Framework的核心组成,而后者则是LINQ to SQL的组成部分。前者已经有越来越多的数据库提供了相应的Provider支持LINQ查询和Entity生成,后者仍仅限于MS SQL Server。

《Pro LINQ in C# 2010》是新近出版的。该书在保留了原2008版所有内容的基础上,主要增加了LINQ to Entity以及Parallel LINQ的内容。可是不知道什么原因,2010版在这两个非常重大的LINQ技术改进上,却没有用到应有的笔墨,实在令人遗憾。

从个人感受讲,Pro系列的书大多只适合于从入门到中级,更深入的内容是无法从中获得的。所以,我最近有了一本O'Reilly出版的《Programming Entity Framework》,相信可以从中获得我需要的内容。

我通常会从LINQ的Wiki获取最新的Provider列表:http://en.wikipedia.org/wiki/Language_Integrated_Query

初识LINQ to Entity

之前有了LINQ to SQL Entity,理解LINQ to Entity时便免不了对二者进行比较。LINQ to Entity被设计与所有支持ADO.NET的数据库进行交互,其大部分操作与LINQ to SQL Entity类似,类的映射关系也与之大部相似。但是,LINQ to Entity更复杂,也更具功能性,已经有了面向对象数据库的雏形。

与SQL Entity比较,可以简单地对LINQ to Entity中作如下理解:ObjectsContext映射数据库,ObjectContext.Entity映射表,Entity映射行,Entity.Property映射列。对应于SQL Entity中由EntityRef<T>与EntitySet<T>建立的Relationship,在LINQ to Entity中有EntityReference<T>与EntityCollection<T>(其中T为子表对应Entity类)实现的Association。

在Visual Studio 2010里,LINQ to SQL Entity是通过“LINQ to SQL类”(.dbml)生成,需要手工从数据库向设计器里拖放表、视图和存储过程等。LINQ to Entity则是通过“ADO.NET实体数据模型”(.edmx)生成,可以在向导里勾选需要的表、视图和存储过程等。对应单个的LINQ to Entity类,还有“ADO.NET自跟踪实体生成器”与“ADO.NET Entity Object生成器”。

LINQ to Entity常见操作

LINQ to Entity的常见CUD操作与SQL Entity极其类似,因此以下只是对书中没有明示的,或者自己有疑问的做了进一步的探索。

SaveChanges()支持Entity对象的自动回滚吗?

我用下面的一段测试代码,确定SubmitChanges()变成SaveChanges()后,同样没有支持Entity对象的回滚。

#region 1: insert a row with dulplicate PK.
Customer c = Customer.CreateCustomer("ALFKI", "My company");
db.Customers.AddObject(c);
try
{
    db.SaveChanges();
    Console.WriteLine("insertion successes.");
}
catch (OptimisticConcurrencyException)
{ Console.WriteLine("conflict happens."); }
catch (UpdateException)
{ Console.WriteLine("update exception happens."); }
#endregion


#region 2: modify the customer constructed.
Console.WriteLine("modify id to AAAAA");
c.CustomerID = "AAAAA";
db.Customers.AddObject(c);
try
{
    db.SaveChanges();
    Console.WriteLine("insertion successes.");
}
catch (OptimisticConcurrencyException)
{ Console.WriteLine("conflict happens."); }
catch (UpdateException)
{ Console.WriteLine("update exception happens."); }
#endregion

#region 3: delete the customer inserted before.
db.Customers.DeleteObject(c);
#endregion

#region 4: construct a new customer to insert.
Console.WriteLine("insert a new customer.");
Customer c2 = Customer.CreateCustomer("AAAAA", "Test2");
db.Customers.AddObject(c2);
try
{
    db.SaveChanges();
    Console.WriteLine("insertion successes.");
}
catch (OptimisticConcurrencyException)
{ Console.WriteLine("conflict happens."); }
catch (UpdateException)
{ Console.WriteLine("update exception happens."); }
#endregion

上述3段代码,1会触发1次Update异常,1+2能成功添加,1+4会触发2次Update异常,1+3+4能成功添加。

级联表的添加与删除

对存在父子关系的两张表,在添加时尽量从父表的角度出发,这样即便要删除父表中的行,其关联的子表行也将被自动删除(没有预先设置级联却也可以,Why?)。反之,苦从子表角度出发,则需要显式地先删除子表的行,再删除父表中的行。

编译后的LINQ查询(MSDN称其为“缓存的LINQ查询”)

注意LINQ to Entity的编译后查询,需要引用的命名空间为System.Data.Objects,而不是System.Data.Linq。这两个空间内,都有对应的CompiledQuery类定义。此前,我一直使用同一个方案在学习LINQ,因此没有正确地引用LINQ to Entity的命名空间,导致我自己编写的编译后查询老是无法通过编译器检查。

样式如下:

Func<ObjectContext, 传入参数类型1~n, 返回值类型> 编译后查询名称或方法名

= CompiledQuery.Compile<ObjectContext, 传入参数类型1~n, 返回值类型>

((context, 传入参数1~n) => LINQ查询语句);

查看LINQ to Entity生成的SQL语句

ObjectContext没有再象DataContext一样暴露Log属性,供客户查阅LINQ生成的SQL查询语句。因此只有变相地使用下面这样的方法,通过ObjectQuery.ToTraceString()获得该查询语句。

IQueryable<Customer> londonCustomers = from c in db.Customers
                                        where c.City == "LONDON"
                                        select c;
// ensure that the database connection is open
if (db.Connection.State != ConnectionState.Open)
{
    db.Connection.Open();
}
// display the sql statement
string sqlStatement = (londonCustomers as ObjectQuery).ToTraceString();

关联数据的加载

类似于LINQ to SQL中使用DataContext.LoadWith()强制地改变关联表的加载时机,在LINQ to Entity里,为每一个Entity类提供了一个Include()方法,用以强制加载其子表。这个Include()可以被放进编译后的LINQ本义定义中。其利弊如下:

利:只取需要的数据,避免无谓的数据加载。

弊:一旦要引用子表数据,将会为子表中的每一行数据引用生成一条SQL查询语句,从而影响查询效率。

在使用Include()过程中,要特别注意以下几点:

1. 如果设置了ObjectContext.ContextOptions.LazyLoadingEnabled = false; 则当你引用未被加载的子表数据时将会引发异常。

2. Include()的参数为父表对象中子表对应的Property名称字符串,比如Customer中的"Orders",而不是子表对象或者其他什么东西。

3. 如果要指定只加载子表中的特定行,则采取类似下述的方法进行显式的加载。其中的关键在于先置ObjectContext的LazyLoadingEnabled为false,再合理地使用条件判断与Orders.Load()方法、Orders.IsLoaded属性配合。

IQueryable<Customer> custs = db.Customers
                .Where(c => c.Country == "UK" && c.City == "London")
                .OrderBy(c => c.CustomerID)
                .Select(c => c);
// explicitly load the orders for each customer
foreach (Customer c in custs)
{
    if (c.CompanyName != "North/South")
    {
        c.Orders.Load();
    }
}

foreach (Customer c in custs)
{
    Console.WriteLine("{0} - {1}", c.CompanyName, c.ContactName);
    // check to see that the order data is loaded for this customerif (cust.Orders.IsLoaded) {
    if (c.Orders.IsLoaded)
    {
        Order firstOrder = c.Orders.First();
        Console.WriteLine(" {0}", firstOrder.OrderID);
    }
    else
    {
        Console.WriteLine(" No order data loaded");
    }
}

使用存储过程

打开*.edmx文件,才能打开实体数据模型浏览器窗口。打开后,主要有两个分支。其中的xxxxxModel是生成的Entity模型,xxxxxModel.Store对应的数据库。

选择存储过程->定义Entity环境下的方法名->取得存储过程各列信息->创建新的复杂类型->为新的复杂类型定义名称,这是将存储过程导入Entity模型的基本步骤。

删除关联的Entity对象

当删除父表中一行时,由于存在子表外键约束,会触发异常。为此,通常的做法是先删除所有的子表关联行,再删除父表中的行,最后再通过SaveChanges()提交给数据库。为了简化方法,LINQ to Entity提供了级联删除的功能。通过在实体模型浏览器中,先选择数据库中的父类,然后在其Keys中选择对应子表外键FK,设置Delete Rule为Cascade。然后再选择ORM中的关联(Association),找到对应的FK约束,同样设置OnDelete属性为Cascade。

LINQ to Entity的冲突处理

LINQ to Entity仍旧采用了开放式的并发模式,而且相对于P557中SQL Entity的并发冲突检测与处理机制而言,有所简化,核心就是决定是Client或者Database“胜出”。

至于造成冲突的方式,仍旧沿用了在SQL Entity中采取的办法:先用Entity读取数据->用ADO.NET修改数据->修改Entity当前数据->经由LINQ to Entity提交给数据库。

但与LINQ to SQL不同的是,Entity的冲突检测不再是设置Column的IsVersionColumn与UpdateCheck特性,而是在对象模型浏览器里,在对应Column的Entity特定Property上设置其Concurrency Mode为Fixed(默认为None)。

ObjectContext仍是处理并发冲突的主体,类似于DataContext.Resove(),通过为其方法Refresh()提供恰当的RefreshMode,以及要被刷新的对象,即可实现冲突处理。

RefreshMode.StoreWins

最终Entity的值被刷新,数据库胜出。

RefreshMode.ClientWins

最终数据库值被刷新,Entity胜出。

作者在P721提供了一个反复提交更新的结构,挺有趣的:

int maxAttempts = 5;
bool recordsUpdated = false;
for (int i = 0; i < maxAttempts && !recordsUpdated; i++)
{
    Console.WriteLine("Performing write attempt {0}", i);
    // save the changes
    try
    {
        context.SaveChanges();
        recordsUpdated = true;
    }
    catch (OptimisticConcurrencyException)
    {
        Console.WriteLine("Detected concurrency conflict - refreshing data");
        context.Refresh(RefreshMode.ClientWins, cust);
    }
}

LINQ to Entity与LINQ to SQL Entity的区别

差异主要是因为比较器无法被转换为数据源,导致使用IEqualityComparer、IComparer接口的比较子的运算符不被LINQ to Entity支持。(对如何自定义Entity比较子,我将在O'Reilly的那本书中去寻求答案。)

参见MSDN:支持和不支持的LINQ方法

一切的一切,留待《Programming Entity Framework》解决……


[LINQ] Pro LINQ 之六:并行LINQ查询

原文地址:https://www.cnblogs.com/Abbey/p/2122780.html