Entity Framework技巧系列之五 - Tip 16 – 19

提示16. 当前如何模拟.NET 4.0ObjectSet<T>

 

背景:

当前要成为一名EF的高级用户,你确实需要熟悉EntitySet。例如,你需要理解EntitySet以便使用 AttachTo(…) 或创建EntityKey

在大部分情况下,针对每个对象/clr类型只有一个可能的EntitySetTip 13正是利用这种想法来简化附加(Attach)对象并且你也可以对Add使用类似的技巧。

然而为了在.NET 4.0中解决这个问题,我们添加了一个叫做 ObjectSet<T> 的新类。ObjectContext中表示一个EntitySet属性的属性的返回类型使用 ObjectSet<T> 替代了 ObjectQuery<T> ObjectSet<T>ObjectQuery<T>不同,因为其不仅支持查询,也允许你进行AddAttach实体。

所以不要再这样写:

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);

现在为了实习这些方法,我们仅需要两个东西:

  1. ObjectContext,这样我们可以真正的执行添加与附加。
  2. ObjectQuery<T>关联的EntitySet的名称。

ObjectContext不值一提。有一个属性包含一个称为ContextObjectQuery可以给我们所需的东西。

得到的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它。

但是到目前为止所有提示中使用了相同的基本模式:

  1. 附加Entity的原始(original)版本。
  2. 修改它
  3. 保存更改(SaveChanges)

在提示13与提示16中我给出提示怎样简化第(1)步操作。在提示7与提示9中分别提到了在步骤(2)操作中你需要理解的问题。

但如果你想将步骤(1)(2)合二为一应该怎样做呢。

例如,你已经有一个修改过的对象,并且你仅想附加它。这就是 AttachAsModified(…) 登场的地方。

 

解决方案:

在这一步有两个我们想要处理的核心任务:

  1. 附加entity(在这一步,EF认为其处于未更改状态)
  2. entity的每一个属性标记为被修改。

要完成这个,你可以使用提示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 }

这段代码所做的就是找到附加的entityObjectStateEntry,并遍历其所有的属性来将它们标记为被修改。

使用这些扩展方法替换,可以编写如下这样代码:

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。对那个规则有一些例外及一个解决方案,但是对于大多数情况这些方法只是不推荐的。

  • 如果你的ObjectContext仅仅进行NoTracking查询,这样其不会变大,因为ObjectContext立即忘掉这些实体。
  • 你可以实现一些非常明确的清理逻辑,如,实现一些类型的回收接口,实现这些接口会遍历ObjectStateManager来分离实体并 AcceptingChanges(..) 来丢弃已删除的对象。注意:我不推荐这样做,我只是说这样做可能,我不知道如果相比较消遣是否这会导致任何的节省。所以这可能是将来一篇文章的一个很好的主题。

l  线程安全:

如果你正试图重用一个ObjectContext,你应该明白那不是线程安全的,例如,类似于标准.NET集合类。如果你由许多线程(如,web请求)访问它,你讲需要手动确保你的同步访问。

l  无状态:

如果服务被设计为无状态,就像大多数web服务应该的那样,在请求之间重用一个ObjectContext可能不是最佳方案。因为上次调用在ObjectContext留下的东西可能对你的应用产生你不期望看到的微妙的影响。

l  自然限制的生存期:

如果你有一个自然限制生存期的方法来管理ObjectContext,如一个短暂存在的表单,一个UnitOfWork或一个Repository,这样将ObjectContext限定在相应的范围内可能是要做的最好的处理。

 

正如你看到的有很多问题在起作用。

大多数这些问题都可以通过使用一个不被共享的短暂生存期的context来解决。

所以那就是我推荐的实用方法。

然而,始终理解实用方法背后的原因将帮助你了解什么时候最适合“走自己的路”。

 

提示19 – 怎样在Entity Framework中使用乐观并发

这是正在进行中的Entity Framework提示系列的第19篇。

背景:

如果你有一个含有timestamp列的表,通过逆向工程由这个表生成一个实体,最终在实体中将有一个Binary类型的属性(在我的例子中称为Version)。

如果你查看属性窗口中那个属性,你讲看到如下这样的东西: 

clip_image001

 

此处有趣的事情是并发模式(Concurrency Mode)。Entity Framework支持2中并发模式:

  • None这是默认值,意味着这个属性不会参与任何并发检查
  • Fixed这意味着此属性的原始值将被作为所有updatedeleteWHERE子句的一部分

由图可知timestamp的并发模式为Fixed,这意味着由数据库原始值会被包含在任何UpdateDeleteWHERE子句中。

如果你更深入一步,在XML编辑器中打开EDMX你可以看到Storage Model(亦称SSDL),你将注意到一些其他的东西,即,Version属性有一个值为ComputedStoreGeneratedPattern属性。

StoreGeneratedPattern3个可能的值:

  • None这是默认值且显然是最常见的。这意味着此列在数据库中不被生成。
  • Identity这意味着当EF进行数据库插入时将生成一个值。所以在一次插入后,EF将获取这个生成的值并将其填充入Entity返回。这个设置频繁用于在数据库中自动生成的主键。
  • Computed这意味着无论EF进行插入或更新时,数据库都将生成一个新值。在插入与更新后,EF将生成值填充入实体并返回。正如你猜到的这一般用于如时间戳(Timestamp)之类的东西。

 

处理乐观并发异常:

在下面的例子中,我有一个称为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

clip_image002

现在如果我们进一步深入,点击查看详细你将看到OptimisticConcurrencyException通过StateEntries属性允许你访问与引起并发异常的实体相关的ObjectStateEntry(s)

clip_image003

这意味着如果你想要优雅的处理此类情况,你只需简单的捕获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 }

很简单,如果你问我。

当然现实中的事情这么简单的很少。你的需求可能是你不得不给用户补偿失败操作并重试的能力。你该怎样正确的完成呢?

嗯,这确是一个有趣的场景,所以期待即将到来的另一个提示。

 

你可能感兴趣的:(framework)