(翻译)Entity Framework技巧系列之八 - Tip 29 – 34
提示29. 怎样避免延迟加载或Load()阅读器问题
如果你有如下这样的代码:
1 var results = from c in ctx.Customers 2 where c.SalesPerson.EmailAddress == “…” 3 select c; 4 foreach(var customer in results) 5 { 6 Console.WriteLine(customer.Name); 7 if (IsInteresting(customer)) 8 { 9 customer.Orders.Load(); 10 foreach(var order in customer.Orders) 11 { 12 Console.WriteLine(“\t” + order.Value); 13 } 14 } 15 }
这段代码将会打开2个同步的阅读器。一个枚举Customers,另一个枚举当前Customer的Orders。并且仅当 Multiple Active ResultSets(又称MARS)启用时才可用。所以如果MARS未启用你讲得到一个令人不快的异常。
注意:你可能会疑问为什么我要在这里调用 IsInteresting(..) 。因为如果没有这个判断,这种模式的代码是明确不推荐的。如果可以避免你不应该这样在循环中访问数据库,换句话说,如果你预先知道需要所有Customer的Order,你应该使用Include()来预先加载订单。
启用MARS很容易,只需将连接字符串中Multiple Active ResultSets=true;即可。
一般情况下不需要自己做,因为大多数连接字符串都是EF设计器创建的,其会为你进行这个设置。在3.5数据库优先及4.0模型优先的情况下都是如此。
但是如果"你"提供了ConnectionString,如Code Only中那样,你需要记得开启MARS。
因为这对3.5和4.0都有效,所以如果出错这两者机会也是均等的。
但是在4.0错误可能更隐蔽,因为新的LazyLoading特性(之前称作DeferredLoading)。
总之,问题的主旨是记得打开MARS!
提示30. 怎样使用自定义数据库函数(UDF)
想象你有一个像Nerd Dinner中DistanceBetween函数这样的数据库函数:
1 CREATE FUNCTION [dbo].[DistanceBetween]( 2 @Lat1 as real, 3 @Long1 as real, 4 @Lat2 as real, 5 @Long2 as real) 6 RETURNS real 7 AS 8 BEGIN 9 … 10 END
你想在Entity Framework使用它
声明这个函数
第一步在XML编辑器中打开EDMX文件,在<edmx:StorageModels>元素中<Schema>下添加一个<Function>元素。
完成后应该如下:
1 <Function Name="DistanceBetween" 2 IsComposable="true" 3 Schema="dbo" 4 Aggregate="false" 5 BuiltIn="false" 6 ReturnType="float"> 7 <Parameter Name="Lat1" Type="float" Mode="In"/> 8 <Parameter Name="Long1" Type="float" Mode="In"/> 9 <Parameter Name="Lat2" Type="float" Mode="In"/> 10 <Parameter Name="Long2" Type="float" Mode="In"/> 11 </Function>
在eSQL中使用函数
现在可以在eSQL中调用这个函数:
1 SELECT VALUE(D) FROM MyModel.Dinners AS D 2 WHERE StorageNamespace.DistanceBetween( 3 D.Latitude,D.Longitude,-34,174) < 50
MyModel就是你EntityContainer的名称(通常与ObjectContext相同),StorageNamespace是你的存储模型模式的命名空间。
在LINQ中使用函数
大部分人不使用eSQL,所以你可能会疑问怎样在LINQ中使用?
在3.5SP1中如下这样:
1 var nearbyDinners = 2 from d in ctx.Dinners.Where( 3 “StorageNamespace.DistanceBetween(it.Latitude, it.Longitude, –34, 174) < 50” 4 ) select d;
这里我们通过一个查询构造方法混合使用LINQ与eSQL,这个方法接收一个eSQL片段,在eSQL中调用了数据库函数。注意代码段通过'it'关键字关联到当前项。如果需要甚至可以关联到参数。
这很赞。
但是如果没有字符串会更好。
EF 4.0中的改进
在EF 4.0中你可以编写下面这样的代码来代替:
1 var nearbyDinners = 2 from d in ctx.Dinners 3 where DistanceBetween(d.Latitude, d.Longitude, –34,174) < 50 4 select d;
这看起来更好。没有上面那样的字符串并且支持编译时检查。
你需要一个这样的方法来使上面的代码工作:
1 [EdmFunction("StorageNamespace", "DistanceBetween")] 2 public double DistanceBetween( 3 double lat1, 4 double long1, 5 double lat2, 6 double long2) 7 { 8 throw new NotImplementedException("You can only call this method as part of a LINQ expression"); 9 }
你可能会疑问为什么这个方法抛出一个异常?
我们从不真正需要直接执行这个方法。我们仅仅用它来编写LINQ查询,此查询会被翻译为SQL而不是实际调用这个方法。
EF使用EdmFuncation特性来得知哪个数据库函数需要代替这个函数被调用。
很酷吧。
好好享受。
提示31. 怎样组合L2O(LINQ to Objects)与L2E(LINQ to Entities)查询
考虑你想写一个如下这样的查询:
1 var possibleBuyers= 2 from p in ctx.People 3 where p.Address.City == “Sammamish” && InMarketForAHouse(p) 4 select p;
理论上只要InMarketForAHouse可以翻译为SQL这段代码就可以执行。
在EF4.0中可以通过为需要的Model或数据库函数创建一个CLR stub来实现。
假设如果没有对应的SQL。
可能这个功能需要使用所有那些不属于数据库的东西。
现在你不得不对查询进行“分割”。例如,将查询分割为基础的LINQ to Entities查询和依赖于L2E的LINQ to Objects查询。
你可能会尝试下如下这样的代码:
1 var partialFilter = from p in ctx.People 2 where p.Address.City == “Sammamish” 3 select p; 4 var possibleBuyers = from p in partiallyFilter 5 where InMarketForAHouse(p); 6 select p;
但是这几乎对代码的行为没有任何作用。 IQueryable(ctx.People) 仍然会被要求将 InMarketForAHouse(..) 翻译为SQL。
你需要调用 AsEnumerable() 方法,其可以有效地将查询独立为两部分:
1 var possibleBuyers = from p in partiallyFilter.AsEnumerable() 2 where InMarketForAHouse(p); 3 select p;
AsEnumerable() 确保LINQ to Objects处理所有的随后的请求。所以LINQ to Entities提供程序(如,ctx.People)用户不会知道 InMarketForAHouse() 方法。
当然有一些警告。
虽然最终的查询可能仅迭代一小部分记录,而实际发送到数据库的查询可能返回大量数据。
所以你需要考虑这将会发生什么。
问自己这样的问题:我会由数据库得到多少数据?
你甚至可能认为“迭代”大量数据没有问题。
问题是默认下你不仅仅在迭代记录。ObjectContext也为每个Entity进行标识识别,包括那些在后续LINQ to Objects中被丢弃的实体,这相当耗资源。
这个特定的问题可以“简单”的使用一个NoTracking查询来避开。
但是这又导致另一系列问题,你不能更新结果集,除非附加它们。
总之希望下一次你需要“分割”查询时你可以更多知道怎样权衡利弊。
提示32. 怎样由SSDL创建一个数据库 – 仅EF4.0
最近我们发布了一个扩展EF4 Beta 1的包含Code Only特性的CTP版本。
你可以在这里,这里与这里了解更多关于Code Only的信息。
如果你查看Code Only的代码走查,你将看到类似下面的代码:
1 // Create a builder and configure it 2 var builder = new ContextBuilder<MyContext>(); 3 … 4 // Create a context 5 var mycontext = builder.Create(sqlConnection); 6 // Prepare the Context 7 if (!myContext.DatabaseExists()) 8 myContext.CreateDatabase();
CreateDatabase() , DropDatabase() , DatabaseExists() 与 CreateDatabaseScripts() 均为发布于Code Only程序集的扩展方法。
这就是很优雅的事情:这些扩展方法与Code Only的剩余部分正相交。
你可以在任何ObjectContext中使用这些扩展方法,而不管其是否由Code-Only创建。
所以你可以在*任何*ObjectContext上调用这些方法。
想象这个场景:你团队中其他人签入一个EDMX作为项目的一部分,但是当你签出后你发现没有数据库脚本。现在你使用Code-Only来创建一个本地数据库。
这些着眼于数据库模型,例如生成并执行DDL,的方法在ObjectContext.MetadataWorkspace中有描述。
伴随这些提示总是有一些警告*:
1. 当前这仅工作于EF4 Beta1。当它们终止的时候我们将发布Code Only的新版本以与EF4较新的版本一同工作。
2. CreateDatabase()不知道怎样处理存储模型中的所有东西。例如如果你的EDMX引用到数据库视图或存储过程,Code Only将不知道怎样生成等效的数据库对象。
3. 当前这仅可与SQL Server一起工作。我们有计划给Code-Only添加提供程序模型,但是那还未实现。
尽管存在这些限制,毫无疑问CreateDatabase()与它同伴们会很有用。
编码愉快!
*没有警告这就不是一个提示了
提示33. 在EF中级联删除真正如何工作
考虑在数据库中你基于一个外键关系实现级联删除。
如下:
这个删除规则表名当一个Category被删除时所有相关的Product也被删除。
如果你由数据库生成一个EF模型,你得到的模型与表面看起来与一般的没有什么不同:
但如果你深入XML的CSDL部分,你会看到:
1 <Association Name="FK_Products_Categories"> 2 <End Role="Categories" Type="TipsModel.Store.Categories" Multiplicity="1"> 3 <OnDelete Action="Cascade" /> 4 </End> 5 <End Role="Products" Type="TipsModel.Store.Products" Multiplicity="*" /> 6 <ReferentialConstraint> 7 <Principal Role="Categories"> 8 <PropertyRef Name="ID" /> 9 </Principal> 10 <Dependent Role="Products"> 11 <PropertyRef Name="CategoryID" /> 12 </Dependent> 13 </ReferentialConstraint> 14 </Association>
注意 <OnDelete> 元素,其通知EF,当一个Category被删除时,也*将*删除相关的Product。
我特意使用*将*而不是*应该*,因为EF不对数据库中的级联删除负责。
EF负责在调用 SaveChanges() 后维持ObjectContext的正确。所以EF常识同步ObjectContext到数据库完成预期的级联删除后预期的状态。
关于这个问题的存在一个说法,如果你打开如SqlProfiler之类的工具,你将注意到当一个主要元素被删除时,EF会在它知道的(如,那些被载入ObjectContext的)依赖主元素的实体上触发DELETE请求。
本质上会发生的是Entity Framework认为在删除数据库中主元素时将删除数据库中所有依赖主元素的东西。所以这就产生问题,什么呢,一个多余的DELETE来请求自己,导致已经加载相关对象被由ObjectContext中删除。
关键要注意的是EF*不会*真正检索数据库中的所有依赖实体并执行删除:它仅删除已经存在内存中的有依赖关系的对象。
所以如下是黄金法则:
- 如果你在模型中添加一个级联删除规则,你必须在数据库中有一个相应的DELETE规则。
- 如果由于一些原因你坚持打破规则(1),级联删除仅当你将所有依赖对象加载到内存中时才起作用。
- (2)是*不*被推荐的!!!
虽然我们尽全力使ObjectContext与数据库保持同步,但如果你有多层级联删除这种努力也会失败。
例如,如果你有如下这样的关系:
Category –> Product –> Order
删除一个Category的同时删除其中所有Product进而删除其Order。
EF可能,在极少的情况下,当你删除一个Category时无法与数据库同步。
例如,你有一个通过未加载的Product关联到一个Category的加载的Order,当你删除Category时,EF不知道应该删除Order。
这意味着Order会以unchanged状态留在ObjectContext中,尽管在数据库中其已被删除。
凡事预则立。
提示34. 怎样在EF中使用可更新视图
更新:谢谢Zeeshan指出默认情况下视图返回的实体中非空列最终会作为主键。
想象这种情况,你的数据库中有一个可更新的视图。
下一步你决定在Entity Framework中使用这个视图,所以你进一步导入这个视图。
产生的实体看起来像这样:
正如你所见,每一个属性都有个“钥匙”图标。
因为这个实体基于一个视图,EF不知道那些列组成主键,所以其假定每一个非空列都是主键的一部分。
固定主键
第一步要更改主键。在这个例子中ID是真正的主键。
可以在XML编辑器中打开EDMX,更改EntityType使其由如下这样,即每个属性都关联到<Key>:
变为这样:
一个很重要的需要注意的是,你不得不同时在EDMX的 <edmx:StorageModels> 与 <edmx:ConceptualModes> 节进行这个改动,因为两个模型需要在主键定义上达成一致。
将视图作为表对待
此刻你可以使用Entity Framework来查询Employees。
但是Entity Framework不允许你进行更新。
对于这个问题的一般方法是创建一个存储过程并以函数方式来使用它们。
但是考虑到视图已经具备更新的能力,以上方案显然不是很理想。
幸运的是有一个替代方案:简单的是EF认为此视图就是一个表。
这需要你更改EntitySet中StorageModel的定义。一般情况下开始时看起来像这样:
1 <EntitySet Name="Employees" 2 EntityType="Tip34Model.Store.Employees" 3 store:Type="Views" 4 store:Schema="dbo" 5 store:Name="Employees"> 6 <DefiningQuery>SELECT 7 [Employees].[ID] AS [ID], 8 [Employees].[Firstname] AS [Firstname], 9 [Employees].[Surname] AS [Surname], 10 [Employees].[Email] AS [Email] 11 FROM [dbo].[Employees] AS [Employees] 12 </DefiningQuery> 13 </EntitySet>
为了让其可以被作为表对待,替换为如下这样:
1 <EntitySet Name="Employees" 2 EntityType="Tip34Model.Store.Employees" 3 store:Type="Tables" 4 Schema="dbo" />
现在你可以执行任何CRUD操作。
很容易吧。