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

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

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

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它。

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

  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 }
 

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

  • 如果你的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)。

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

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

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

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

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

StoreGeneratedPattern有3个可能的值:

  • 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:

…现在如果我们进一步深入,点击’查看详细’你将看到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 }

很简单,如果你问我。

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

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

Entity Framework技巧系列之五 - Tip 16 – 19的更多相关文章

  1. Entity Framework技巧系列之六 - Tip 20 – 25

    提示20. 怎样处理固定长度的主键 这是正在进行中的Entity Framework提示系列的第20篇. 固定长度字段填充: 如果你的数据库中有一个固定长度的列,例如像NCHAR(10)类型的列,当你 ...

  2. Entity Framework技巧系列之七 - Tip 26 – 28

    提示26. 怎样避免使用不完整(Stub)实体进行数据库查询 什么是不完整(Stub)实体? 不完整实体是一个部分填充实体,用于替代真实的对象. 例如: 1 Category c = new Cate ...

  3. Entity Framework技巧系列之八 - Tip 29 – 34

    提示29. 怎样避免延迟加载或Load()阅读器问题 如果你有如下这样的代码: 1 var results = from c in ctx.Customers 2 where c.SalesPerso ...

  4. Entity Framework技巧系列之三 - Tip 9 – 12

    提示9. 怎样直接删除一个对象而无需检索它 问题 最常见的删除Entity Framework中实体的方式是将你要删除的实体传入Context中并像如下这样删除: 1 // 按ID查找一个类别 2 / ...

  5. Entity Framework技巧系列之一 - Tip 1 - 5

    提示1. 在Entity Framework中怎样排序关系(Relationships) 问题: 在Entity Framework论坛中常会看到关于排序相关联项目的问题. 例如,想象你要查询客户,并 ...

  6. Entity Framework技巧系列之十三 - Tip 51 - 55

    提示51. 怎样由任意形式的流中加载EF元数据 在提示45中我展示了怎样在运行时生成一个连接字符串,这相当漂亮. 其问题在于它依赖于元数据文件(.csdl .ssdl .msl)存在于本地磁盘上. 但 ...

  7. Entity Framework技巧系列之十二 - Tip 46 - 50

    提示46. 怎样使用Code-Only排除一个属性  这次是一个真正简单的问题,由StackOverflow上这个问题引出.  问题:  当我们使用Code-Only把一个类的信息告诉Entity F ...

  8. (翻译)Entity Framework技巧系列之十 - Tip 37 - 41

    提示37. 怎样进行按条件包含(Conditional Include) 问题 几天前有人在StackOverflow上询问怎样进行按条件包含. 他们打算查询一些实体(比方说Movies),并且希望预 ...

  9. Entity Framework技巧系列之十四 - Tip 56

    提示56. 使用反射提供程序编写一个OData Service 在TechEd我收到一大堆有关将数据作为OData暴露的问题. 到目前为止你大概知道可以使用数据服务与Entity Framework将 ...

随机推荐

  1. RF环境搭建

    官网:http://robotframework.org/ 序号 安装包名 安装方法 下载地址 备注 1 python exe文件,直接双击安装 https://www.python.org/down ...

  2. selenium grid的使用与配置

    一.selenium grid的组成与作用:由一个集线器hub和多个客户机node组成,如果你的程序需要在不用的浏览器,不同的操作系统上测试,而且比较多的case需要多线程远程执行,那么一个比较好的测 ...

  3. Nginx将项目配置在子目录

    问题:一个完整的项目需要整合在另外一个项目中,作为一个子模块存在 有两个项目prject1 根目录/www/project1与project2 /www/project2,现在是想将probject1 ...

  4. pull类型消息中间件-消息服务端(三)

    部署架构 消息存储 存储结构 MetaQ的存储结构是一种物理队列+逻辑队列的结构.如下图所示: Producer生产消息,根据消息的topic选择topic对应某一个分区,然后发送到这个分区对应的Br ...

  5. oracle 使用技巧

    1.PL/SQL Developer记住登陆密码  在使用PL/SQL Developer时,为了工作方便希望PL/SQL Developer记住登录Oracle的用户名和密码: 设置方法:PL/SQ ...

  6. CSU 1640 机智的刷题方式

    完全背包 #include<cstdio> #include<cstring> #include<cmath> #include<algorithm> ...

  7. form里面的action和method(post和get的方法)使用

    一.form里面的action和method的post使用方法 <%@ Page Language="C#" AutoEventWireup="true" ...

  8. iOS开发怎么样做第三方登陆(友盟社会化分享)

    基于前一篇文章   自定义UI后 实现如下代码   即可 //第三方登陆 //    UMSocialSnsPlatform *snsPlatform = [UMSocialSnsPlatformMa ...

  9. Node.js:全局对象

    概要:本篇博客主要介绍了node.js中的全局对象. 在JavaScript中,通常window是全局对象,而node.js中的全局对象是global,所有全局变量(除了global本身之外)都是gl ...

  10. java中的日期处理

    学习Java日期处理,看见这一篇比较详细,转载之. 转自:http://www.cnblogs.com/hqr9313/archive/2012/04/19/2458221.html   时间日期1) ...