提示13. 附加一个实体的简单方式

问题:

在早先的一些提示中,我们讨论了使用Attach来加载一个处于未改变(unchanged)状态的东西到ObjectContext从而避免进行查询的开销。

如果性能是你的目标,Attach就是要选择的武器。

不幸的是我们的API不能适应99%的情况,即每个类型仅有一个实体集(entity set)的情况。Entity Framework支持单类型多实体集(Multiple Entity Sets per Type)或称MEST,且API反映了这一点,要求你提供你要附加的实体集(EntitySet)的名称。

即,像这样:

1 ctx.Attach("Orders", order); 

如果你像我一样,可能你也会发反感在代码里硬编码入字符串。它们容易出错且这类东西会污染你的代码,这实质上是一个"小问题"。

.NET 4.0中的解决方案

在.NET 4.0中通过每一个EntitySet返回ObjectSet<T>而不是ObjectQuery<T>这个强类型的属性修复了这个问题。ObjectSet<T>有Add, Delete及Attach方法直接处理这个问题,所以你可以写如下这样的代码:

1 ctx.Order.Attach(order);

没有一个字符串出现!

这种解决方案是理想的,你附加需要的实体集,且无论你是否有MEST,它都工作。

.NET 3.5中的解决方案

.NET 3.5中应该怎么办呢?

我的观点是,我们应该提供一个泛型版本的Attach,即如下这样:

1 void AttachToDefaultSet<T>(T Entity); 

这个方法会检查T中存在多少个EntitySet,如果只有一个,其将附加这个实体到那个实体集。然而如果多于一个其将抛出异常。

虽然这种方法没有扩展方法的力量,但它也很容易写。

以下是你需要做的:

  1. 得到<T>类型的EntityType
  2. 得到这个EntityType可能属于的EntitySet类型的列表。EntityType可能为派生类型(像Car),并且实际上属于一个父类型集合(像Vehicles)。
  3. 遍历EntityContainer,对每一个EntityContainer的EntitySet查找一个匹配。
  4. 如果找到一个则进行附加,否则抛出异常。

下面让我们来完成:

但首先要注意,这只是示例级质量的代码,我是一个项目经理而非程序员,所以使用这些代码的风险自担:)

首先我们添加一个扩展方法到MetadataWorkspace来获取一个CLR类型(O-Space)对应的概念模型(C-Space)EntityType。

 1 public static EntityType GetCSpaceEntityType<T>(
2 this MetadataWorkspace workspace
3 )
4 {
5 if (workspace == null)
6 throw new ArgumentNullException("workspace");
7 // Make sure the assembly for "T" is loaded
8 workspace.LoadFromAssembly(typeof(T).Assembly);
9 // Try to get the ospace type and if that is found
10 // look for the cspace type too.
11 EntityType ospaceEntityType = null;
12 StructuralType cspaceEntityType = null;
13 if (workspace.TryGetItem<EntityType>(
14 typeof(T).FullName,
15 DataSpace.OSpace,
16 out ospaceEntityType))
17 {
18 if (workspace.TryGetEdmSpaceType(
19 ospaceEntityType,
20 out cspaceEntityType))
21 {
22 return cspaceEntityType as EntityType;
23 }
24 }
25 return null;
26 }

由于你可能在<T>的元数据加载前调用这段代码,代码第一行保证了<T>这个程序集被加载,如果程序集已被加载其不会执行任何操作。

下一步我们添加一个方法得到一个我们需要匹配的所有类型的枚举,即,包括当前类型在内的父类型的层级:

 1 public static IEnumerable<EntityType> GetHierarchy(
2 this EntityType entityType)
3 {
4 if (entityType == null)
5 throw new ArgumentNullException("entityType");
6 while (entityType != null)
7 {
8 yield return entityType;
9 entityType = entityType.BaseType as EntityType;
10 }
11 }

最后,我们可以开始完成AttachToDefaultSet方法:

 1 public static void AttachToDefaultSet<T>(
2 this ObjectContext ctx,
3 T entity)
4 {
5 if (ctx== null) throw new ArgumentNullException("ctx");
6 if (entity == null) throw new ArgumentNullException("entity");
7
8
9 MetadataWorkspace wkspace = ctx.MetadataWorkspace;
10 EntitySet set = wkspace
11 .GetEntitySets(wkspace.GetCSpaceEntityType<T>())
12 .Single();
13
14 ctx.AttachTo(set.Name, entity);
15 }

这里使用了标准的.Single()方法,如果不是恰好存在一个对应EntityType的可能的实体集其将抛出一个异常。

使用这个实现,我们可以将前文的代码使用下面这种方式重写:

1 Product p = newProduct { ID = 1, Name = "Chocolate Fish" } ctx.AttachToDefaultSet(p);

当然,除非你使用MEST...但你可能不使用!

附加说明

虽然这段代码可以很好的工作,但其确实没有进行过任何优化。

或许缓存对应一个CLR类型的可能的集合的名字是有意义的,这样当你进行Attach的时候就无需再进行相同的检查,这就当留给你的练习了!

提示索引

是的,这里有一个本系列剩余提示的索引

提示14. 怎样缓存Entity Framework引用数据

场景:

为了使应用程序可以工作,缓存常用的引用数据是非常有意义的。

引用数据的好例子包括像States, Countries, Departments等事物。

通常你想要这些数据随时可用,以方便的进行填充下拉列表等操作。

什么地方将引用数据缓存在手边的一个好例子是让新客户注册的页面,表单的一部分收集用户的地址,包括他们的州。在这个例子中你需要引用数据做两件事:

1. 构建表单中选择州的下拉列表。

2. 将州指定到最终的用户记录。

你怎样使用Entity Framework来支持这类场景呢?

解决方案:

当设计解决方案时我们需要记住两个关键点。

1. 一个实体同一时间只能被附加到一个ObjectContext,至少.NET 3.5 SP1中是这样。

2. 你可能由许多线程同时使用缓存的引用数据(读ObjectContexts)。

本质上这两点相互矛盾。

解决方案是当我们由缓存读取时拷贝实体,这样附加拷贝将不会影响任何其它线程。

如果这是一个webform解决方案,我们可能要写这样的代码:

 1 var customer = new Customer{
2 Firstname = txtFirstname.Text,
3 Surname = txtSurname.Text,
4 Email = txtEmail.Text,
5 Street = txtStreet.Text,
6 City = txtCity.Text,
7 State = statesCache.GetSingle(
8 s => s.ID = Int32.Parse(ddState.SelectedValue)
9 ),
10 Zip = txtZip.Text
11 }
12 ctx.AddToCustomers(customer);
13 ctx.SaveChanges();

但是这有一个大问题。当你添加customer到ObjectContext时,拷贝的State也是added状态。如果我们这样做,Entity Framework会认为其需要将State插入到数据库。而这不是我们想要的。

所以我们需要通过使用 AttachTo(...) 告诉Entity Framework State这个拷贝的已经存在于数据库中:

1 var state = statesCache.GetSingle(
2 s => s.ID = Int32.Parse(ddState.SelectedValue)
3 );
4 // See Tip 13 to avoid specifying the EntitySet
5 // as a string
6 ctx.AttachTo("States", s);

然后我们可以继续构建customer:

 1 var customer = new Customer{
2 Firstname = txtFirstname.Text,
3 Surname = txtSurname.Text,
4 Email = txtEmail.Text,
5 Street = txtStreet.Text,
6 City = txtCity.Text,
7 State = state,
8 Zip = txtZip.Text
9 }
10 ctx.SaveChanges();

如果你足够警惕,你可能已经发现我没有再次调用 AddToCustomers(...) 。

为什么呢?嗯,当你构建一个到已存在于context (State = state)中的关系时,这个customer会自动被添加。

现在,在 SaveChanges() 被调用时,只有Customer被存储到数据库。State根本不会被持久化,因为Entity Framework认为其不曾改变。

有趣的是,我们可以利用State不被支持化的事实作为我们的条件。

因为,State的主键属性是Entity Framework构建关系时唯一需要知道的,即使其它属性都错了也没有关系,主键属性实际上是我们拷贝时唯一需要的。

这样,我们的拷贝代码可以非常简单:

1 public State Clone(State state)
2 {
3 return new State {ID = state.ID};
4 }

或者使用如下lambda表达式:

1 var cloner = (State s) => new State {ID = s.ID};

只要我们不想要修改拷贝,这些就是所有我们实际需要的。

现在我们知道了需要什么,编写一个非常简单的提供缓存与"只读拷贝"服务的泛型类就很容易了。

 1 public class CloningCache<T> where T : class
2 {
3 private List<T> _innerData;
4 private Func<T, T> _cloner;
5 public CloningCache(IEnumerable<T> source, Func<T, T> cloner)
6 {
7 _innerData = source.ToList();
8 _cloner = cloner;
9 }
10 public T GetSingle(Func<T, bool> predicate)
11 {
12 lock (_innerData)
13 {
14 return _innerData
15 .Where(predicate)
16 .Select(s => _cloner(s))
17 .Single();
18 }
19 }
20 }

注意, GetSingle(...) 方法拷贝它找到的结果。

另外使用这个拷贝缓存非常简单:

1 var statesCache = new CloningCache<State>(
2 ctx.States,
3 (State s) => new State {ID = s.ID}
4 );

构造函数的第一个参数是要缓存的数据(即数据库中所有的States),第二个参数是怎样实现拷贝,我们需要跨多个ObjectContext安全的使用这个缓存。

一旦你初始化了这个缓存(大概是在Global.asax中),无论什么情况下你需要直接访问引用数据,你都可以在一个静态变量中情况此缓存。

如果哪里讲的不清楚或你有什么问题,请告诉我。

提示15. 怎样避免加载非必须的属性

更新:对之前需要Original值做了一系列重要的更正。

问题:

想象如果你查询博客随笔:

1 var myPosts = from post in ctx.Posts
2 orderby post.Created descending
3 select post;

仅仅这样你就可以输出博客标题等等。

1 foreach(var post in myPosts)
2 {
3 Console.WriteLine("{0} on {1}", post.Title, post.Created);
4 }

这样你做了一大些无用的工作来加载你实际上不需要的属性。

只读解决方案:

对于只读场景,解决方案很容易。

你只需进行投影操作:

1 var myPosts = from post in ctx.Posts
2 orderby post.Created descending
3 select new {post.Title, post.Created};

这样你就避免了加载你实际上不需要的属性。

对于有许多属性或存在映射到数据库中一个blob列的属性的实体,例如像Body之类映射到一个nvarchar(max)列的属性,这尤其重要。

读写解决方案:

但如果你需要修改实体该怎么办呢?

此处投影不是一个好方案,因为除非你获取一个完整的对象,否则你将不会得到任何对象服务,这意味着将无法更新。

嗯…

一如往常,得到一个解决方案的关键的就是理解Entity Framework的工作方式。

当更新一个实体,Entity Framework将更新以如下格式发送到数据库(伪代码):

 1 UPDATE [Table]
2 SET
3 ModifiedProperty1 = NewValue1,
4 ModifiedProperty2 = NewValue2,
5 ...
6 ModifiedPropertyN = NewValueN
7 WHERE
8 KeyProperty = KeyValue AND
9 ModifiedProperty1 = OriginalValue1 AND
10 ModifiedProperty2 = OriginalValue2 AND
11 ...
12 ModifiedPropertyN = OriginalValueN

注意没有修改过的属性不会出现在更新命令的任何地方。

重大发现:这意味着你只需要知道主键属性的原始值即可。*

带着这些发现,我们可以进行下面这样的尝试:

  1. 仅投射出我们需要读写的列
  2. 由投影伪造一个实体,忽略我们不关心的列。
  3. Attach(...)那个"部分正确"的实体
  4. 对实体做所需的更改
  5. SaveChanges(...)

这样我们就可以在不实例化我们不感兴趣的属性的情况下更改我们的实体。

下面是一些可以完成上述工作的代码:

 1 // Project just the columns we need
2 var myPosts = from post in ctx.Posts
3 orderby post.Created descending
4 select new {post.ID, post.Title};
5 // Fabricate new Entities in memory.
6 // Notice the use of AsEnumerable() to separate the in db query
7 // from the LINQ to Objects construction of Post entities.
8 var fabricatedPosts = from p in myPosts.AsEnumerable()
9 select new Post{ID = p.ID, Title = post.Title};
10 // Now we attach the posts
11 // And call a method to modify the Title
12 foreach(var p in fabricatedPosts)
13 {
14 ctx.AttachTo("Posts", p);
15 p.Title = ChangeTitle(p.Title);
16 }
17 ctx.SaveChanges();

注意我们只检索了ID属性(主键)和Title属性(我们要修改的东西),但我们仍成功地进行了更新。

TA DA!

*警告/并发问题

如果你使用存储过程更新实体,这个提示中的内容不适用。

如果你考虑存储过程工作的方式你可以明白为什么。当使用存储过程进行更新时,所有当前值(及部分原始值)被映射到参数,而不管它们是被修改过。这基本意味着你不得不获得所有原始值:(

另外有些时候你需要告诉Entity Framework一些其它的原始值,因为没有它们更新不会成功:

  • 并发属性:并发属性的原始值被包含在更新语句中,来保证你只可以在明确知道当前数据库版本的情况下更新数据库。所以没有正确的原始值更新不会成功。
  • EntityFramework的EntityKey值:你也需要知道0..1关系的原始值,即使不准备改变此关系。例如如果一个订单有一个客户,你将需要知道CustomerReference.EntityKey,然后你可以使用这个已确立的关系初始化一个新的Order。当你使用FK属性(在即将到来的.NET 4.0中)时,这个问题将不复存在。

C-Side映射条件引用的属性:C-Side映射条件的值用于算出应用了哪个映射,所以没有正确的原始值就不会确立正确的更新映射。大部分人不会使用这个特性。

ntity Framework技巧系列之四 - Tip 13 – 15的更多相关文章

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

    提示16. 当前如何模拟.NET 4.0的ObjectSet<T> 背景: 当前要成为一名EF的高级用户,你确实需要熟悉EntitySet.例如,你需要理解EntitySet以便使用 At ...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

随机推荐

  1. 路由页面缓存开启 以及 keep-alive 给你埋下的坑

    为什么要用keep-alive呢, 因为这个会缓存dom模板, 下次再回到这个页面, 就可以利用这个已经渲染好的dom结构了, 如果数据不一样, 也会启用 virtualDoM 进行diff更新, 这 ...

  2. STL容器小结

     1.空间分配器 std::alloc用于容器中内存空间的分配和释放,以及分配内存的管理.construct().destroy()等全局函数用于为对象的构造和析构. 2.迭代器和trains 迭代器 ...

  3. 数据结构之线性表的顺序存储结构的实现--C语言版

    #include <stdio.h> #include <stdlib.h> #include <time.h> #define INIT_SIZE 100 #de ...

  4. Memcached内存存储

    早就听说过Memcached独特的内存管理方式,写着篇文章的目的就是了解Memcached的内存管理,学习其源代码. 1.什么是Slab Allocator memcached默认情况下采用了名为Sl ...

  5. 二十三、oracle pl/sql分类三 包

    包用于在逻辑上组合过程和函数,它由包规范和包体两部分组成.1).我们可以使用create package命令来创建包,如:i.创建一个包sp_packageii.声明该包有一个过程update_sal ...

  6. 第二次讨论——响应式设计、布局技巧、css性能优化、css预处理

    第二次讨论 [响应式设计] 集中创建页面的图片排版大小,可以智能地根据用户行为以及使用的设备环境(系统平台.屏幕尺寸.屏幕定向等)进行相对应的布局. 响应式布局: meta标签的实用:设置布局宽度等于 ...

  7. L2-007. 家庭房产

    L2-007. 家庭房产 题目链接:https://www.patest.cn/contests/gplt/L2-007 并查集 初学,看这题的时候完全没有什么好的想法,参考了@yinzm的blog用 ...

  8. Ubuntu 14.04 apt源更新

    # 14.04 下进 my /etc/apt/sources.list /etc/apt/sources.bak vi /etc/apt/sources.list 从以下源中选择一个 源列表 Trus ...

  9. LeetCode OJ 169. Majority Element

    Given an array of size n, find the majority element. The majority element is the element that appear ...

  10. [译] Block 小测验

    本文来源于 ParseBlog 的其中一篇博文 <Objective-C Blocks Quiz> 如果您觉得我的博客对您有帮助,请通过关注我的新浪微博  MicroCai 支持我,谢谢! ...