提示16. 当前如何模拟.NET 4.0的ObjectSet<T>
背景:
当前要成为一名EF的高级用户,你确实需要熟悉EntitySet。例如,你需要理解EntitySet以便使用 AttachTo(…) 或创建EntityKey。
在大部分情况下,针对每个对象/clr类型只有一个可能的EntitySet。Tip 13正是利用这种想法来简化附加(Attach)对象并且你也可以对Add使用类似的技巧。
然而为了在.NET 4.0中解决这个问题,我们添加了一个叫做 ObjectSet<T> 的新类。ObjectContext中表示一个EntitySet属性的属性的返回类型使用 ObjectSet<T> 替代了 ObjectQuery<T> 。ObjectSet<T>与ObjectQuery<T>不同,因为其不仅支持查询,也允许你进行Add与Attach实体。
所以不要再这样写:
1 ctx.AddObject(“Customers”, newCustomer);
这种情况下你需要以字符串(我提到过我痛恨字符串吗?)形式指定EntitySet的名称,ObjectSet<T>将允许你这样做:
1 ctx.Customers.AddObject(newCustomer);
不像提示13提出的解决方案,ObjectSet<T>甚至可以在一个对象有多于一个可能的EntitySet,又称MEST,的情况下工作。
ObjectSet<T>也有另一个非常重要的优势。其实现了IObjectSet<T>接口,这样你可以针对接口来编写代码与测试,这意味着可以很容易的伪造或模拟你的ObjectContext。
下一个版本的EF将会相当酷。
但是目前我们怎样在.NET 3.5中模拟这个特性呢?
解决方案:
通过使用扩展方法构建一种天真的解决方案*实际上非常简单。
我们仅仅向ObjectQuery<T>添加一个扩展方法使其看起来像包含一些其它方法:
1 public static void AddObject<T>( 2 this ObjectQuery<T> query, T entity 3 ) 4 public static void Attach<T>( 5 this ObjectQuery<T> query, T entity 6 )
一旦我们实现了这些方法,你可以编写可以在.NET 4.0中编写的相同类型的代码:
1 ctx.Customers.Attach(oldCustomer); 2 ctx.Customers.AddObject(newCustomer);
现在为了实习这些方法,我们仅需要两个东西:
ObjectContext不值一提。有一个属性包含一个称为Context的ObjectQuery可以给我们所需的东西。
得到的EntitySet名称稍有点麻烦,关键点在于使用CommandText属性。此属性通常*就是一个字符串,看起来有些像这样:“EntitySetName”,所以我们需要做的就是去除开头的’[’与结尾的’]’,这样我们就得到EntitySet的名称。
由于所有方法都需要EntitySet的名称,我们写一个函数来获取它:
1 public static string GetEntitySetName<T>( 2 this ObjectQuery<T> query) 3 { 4 string name = query.CommandText; 5 // See Caveat! 6 if (!name.StartsWith("[") || !name.EndsWith("]")) 7 throw new Exception("The EntitySet name can only be established if the query has not been modified"); 8 return name.Substring(1, name.Length - 2); 9 }
其它两个方法完全微不足道:
1 public static void AddObject<T>( 2 this ObjectQuery<T> query, T entity) 3 { 4 string set = query.GetEntitySetName(); 5 query.Context.AddObject(set, entity); 6 } 7 8 public static void Attach<T>( 9 this ObjectQuery<T> query, T entity) 10 { 11 string set = query.GetEntitySetName(); 12 query.Context.AttachTo(set, entity); 13 }
这样就完成了,很容易。
*说明(防误解):
我称这是一个天真的解决方案是因为有可能进行下面这样的操作:
1 context.Customer.Where(“it.Name == ‘MSFT’”).Attach(oldCustomer);
且这将会失败。这是由于上面代码段中对Where(..)的调用修改了查询的CommandText,而我们使用一个非常天真的函数由CommandText提取名称。
这个函数的整体目标仍然是易于使用,且你不太可能会写那样的代码,所以总之可能一个天真的解决方案可能刚刚好。
提示17. 怎样使用AttachAsModified(..)进行一次性更新
背景:
在提示13 – 怎样以简单方式附加这篇随笔中,我展示了怎样’确定’一个特殊CLR类型的EntitySet,以便你可以Attach它。
但是到目前为止所有提示中使用了相同的基本模式:
在提示13与提示16中我给出提示怎样简化第(1)步操作。在提示7与提示9中分别提到了在步骤(2)操作中你需要理解的问题。
但如果你想将步骤(1)和(2)合二为一应该怎样做呢。
例如,你已经有一个修改过的对象,并且你仅想附加它。这就是 AttachAsModified(…) 登场的地方。
解决方案:
在这一步有两个我们想要处理的核心任务:
要完成这个,你可以使用提示13中展示的默认实体集(entity set)的思想,直接向ObjectContext添加一个扩展方法。
但取而代之的是,我将基于提示16的思想来构建,在ObjectQuery<T>上添加另一个扩展方法,如下这样:
1 public static void AttachAsModified<TEntity>( 2 this ObjectQuery<TEntity> query, 3 TEntity entity) where TEntity : EntityObject 4 { 5 if (query == null) throw new ArgumentNullException("query"); 6 if (entity == null) throw new ArgumentNullException("entity"); 7 // Uses method created in Tip 16 8 query.Attach(entity); 9 query.Context 10 .ObjectStateManager 11 .MarkAllPropertiesModified(entity); 12 }
Attach(..) 方法在提示16中实现,所以现在我仅需添加的是一个 MarkAllPropertiesModified(…) 实现。
虽然此实现使用了ObjectStateManager内部一些隐藏的代码,但实际上它非常的简单:
1 public static void MarkAllPropertiesModified<TEntity>( 2 this ObjectStateManager manager, 3 TEntity entity) where TEntity : EntityObject 4 { 5 if (manager== null) 6 throw new ArgumentNullException("manager"); 7 if (entity == null) 8 throw new ArgumentNullException("entity"); 9 10 // get the object state entry for the Entity 11 var entry = manager.GetObjectStateEntry(entity); 12 13 // use metadata to get all the property names 14 // this is quicker and safer than reflection, 15 // because it ignores properties not in the model 16 var propNames = 17 from x in entry.CurrentValues.DataRecordInfo.FieldMetadata 18 select x.FieldType.Name; 19 20 // loop over every property and mark it modified 21 foreach (var propName in propNames) 22 entry.SetModifiedProperty(propName); 23 }
这段代码所做的就是找到附加的entity的ObjectStateEntry,并遍历其所有的属性来将它们标记为被修改。
使用这些扩展方法替换,可以编写如下这样代码:
1 Customer updatedCustomer = GetUpdatedEntity(); 2 ctx.Customers.AttachAsModified(updatedCustomer); 3 ctx.SaveChanges();
非常简单不是吗?
注意首次我不必要在附加代码前后实行我的更改。所有对实体有趣的更改是在将实体附加到context之前完成的。
这使得在你的代码中创建层变得更加的容易。
说明(防误解):
在.NET 3.5 SP1中由于通过普通引用或缺少外键属性会引起一些问题。
如果你想要更新引用属性(如: customer.SalesPerson ),你将需要在附加逻辑的前后重新引入一些东西。
在调用 AttachAsModified(…) 之前,你可以更新所有的属性但不包括引用。引用将需要为它们的原始值,这是由于Entity Framework处理独立关联(Independent Association)的方式。
然后在调用 AttachAsModified(…) 之后,你将需要使用最新的值更新引用,例如,这样的代码( customer.SalesPersonReference.EntityKey = … )
参见提示7获得更多关于这个话题的内容。
提示18. 怎样决定你的ObjectContext的生存期
我们收到的一个最常见的问题是一个ObjectContext应该存在多久。常提到的选项包括下面之一:
很多人正在询问这些类型的问题,此处一个相关的例子是Stackflow上的一个问题。我相信在我们论坛上也有更多的问题被掩盖了。
这是一个典型的it depends(这取决于)类型的问题。
大量因素用于权衡问题的决策,包括:
l 析构(Disposal):
干净的析构ObjectContext及其资源是重要的。如果你为每个函数创建一个新的ObjectContext同样是非常的容易,因为你可以简单的写一个using块来确保资源被恰当的析构:
1 using (MyContext ctx = new MyContext()) 2 { 3 }
l 构造成本:
一些人们关心一次又一次重建ObjectContext对象的开销,这很容易理解。现实是这个开销实际上相当低,因为通常其只涉及以引用方式将元数据由一个全局缓存复制到新的ObjectContext。通常我不认为这个开销值得担忧,但一如既往,对那个规则将有一个例外。
l 内存使用:
你越多的使用一个ObjectContext,其将逐渐的变大。这是因为其保持一个到所有其知道的实体的引用,尤其是你查询过的,添加或附加过的任何东西。所以你应当重新考虑是否无限制的共享同一个ObjectContext。对那个规则有一些例外及一个解决方案,但是对于大多数情况这些方法只是不推荐的。
l 线程安全:
如果你正试图重用一个ObjectContext,你应该明白那不是线程安全的,例如,类似于标准.NET集合类。如果你由许多线程(如,web请求)访问它,你讲需要手动确保你的同步访问。
l 无状态:
如果服务被设计为无状态,就像大多数web服务应该的那样,在请求之间重用一个ObjectContext可能不是最佳方案。因为上次调用在ObjectContext中留下的东西可能对你的应用产生你不期望看到的微妙的影响。
l 自然限制的生存期:
如果你有一个自然限制生存期的方法来管理ObjectContext,如一个短暂存在的表单,一个UnitOfWork或一个Repository,这样将ObjectContext限定在相应的范围内可能是要做的最好的处理。
正如你看到的有很多问题在起作用。
大多数这些问题都可以通过使用一个不被共享的短暂生存期的context来解决。
所以那就是我推荐的实用方法。
然而,始终理解’实用方法’背后的原因将帮助你了解什么时候最适合“走自己的路”。
提示19 – 怎样在Entity Framework中使用乐观并发
这是正在进行中的Entity Framework提示系列的第19篇。
背景:
如果你有一个含有timestamp列的表,通过逆向工程由这个表生成一个实体,最终在实体中将有一个Binary类型的属性(在我的例子中称为Version)。
如果你查看属性窗口中那个属性,你讲看到如下这样的东西:
此处有趣的事情是并发模式(Concurrency Mode)。Entity Framework支持2中并发模式:
由图可知timestamp的并发模式为Fixed,这意味着由数据库原始值会被包含在任何Update或Delete的WHERE子句中。
如果你更深入一步,在XML编辑器中打开EDMX你可以看到Storage Model(亦称SSDL),你将注意到一些其他的东西,即,Version属性有一个值为Computed的StoreGeneratedPattern属性。
StoreGeneratedPattern有3个可能的值:
处理乐观并发异常:
在下面的例子中,我有一个称为Post的实体,其中包含一个称为Version的时间戳列。给出下面这段简单的代码:
1 using (TipsEntities ctx1 = new TipsEntities()) 2 { 3 // Get a post (which has a Version Timestamp) in one context 4 // and modify. 5 Post postFromCtx1 = ctx1.Post.First(p => p.ID == 1); 6 postFromCtx1.Title = "New Title"; 7 8 // Modify and Save the same post in another context 9 // i.e. mimicking concurrent access. 10 using (TipsEntities ctx2 = new TipsEntities()) 11 { 12 Post postFromCtx2 = ctx2.Post.First(p => p.ID == 1); 13 postFromCtx2.Title = "Newer Title"; 14 ctx2.SaveChanges(); 15 } 16 // Save the changes... This will result in an Exception 17 ctx1.SaveChanges(); 18 }
…将引发一个OptimisticConcurrencyException:
…现在如果我们进一步深入,点击’查看详细’你将看到OptimisticConcurrencyException通过StateEntries属性允许你访问与引起并发异常的实体相关的ObjectStateEntry(s):
这意味着如果你想要优雅的处理此类情况,你只需简单的捕获OptimisticConcurrencyException并获取与那些StateEntries相关的实体,一边你可以提供一些类型的信息:
1 catch (OptimisticConcurrencyException ex) { 2 ObjectStateEntry entry = ex.StateEntries[0]; 3 Post post = entry.Entity as Post; 4 Console.WriteLine("Failed to save {0} because it was changed in the database", post.Title); 5 }
很简单,如果你问我。
当然现实中的事情这么简单的很少。你的需求可能是你不得不给用户补偿失败操作并重试的能力。你该怎样正确的完成呢?
嗯,这确是一个有趣的场景,所以期待即将到来的另一个提示。