引言
《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方法