ntity Framework技巧系列之四 - Tip 13 – 15

提示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映射条件的值用于算出应用了哪个映射,所以没有正确的原始值就不会确立正确的更新映射。大部分人不会使用这个特性。

时间: 2024-10-05 13:05:04

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

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

提示16. 当前如何模拟.NET 4.0的ObjectSet<T> 背景: 当前要成为一名EF的高级用户,你确实需要熟悉EntitySet.例如,你需要理解EntitySet以便使用 AttachTo(-) 或创建EntityKey. 在大部分情况下,针对每个对象/clr类型只有一个可能的EntitySet.Tip 13正是利用这种想法来简化附加(Attach)对象并且你也可以对Add使用类似的技巧. 然而为了在.NET 4.0中解决这个问题,我们添加了一个叫做 ObjectSet<T&

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

提示20. 怎样处理固定长度的主键 这是正在进行中的Entity Framework提示系列的第20篇. 固定长度字段填充: 如果你的数据库中有一个固定长度的列,例如像NCHAR(10)类型的列,当你进行一次插入时,填充会自动发生.所以例如如果你插入'12345',你将得到5个自动填充的空格,来创建一个10个字符长度的字符串. 大多数情况下,这种自动填充不会有问题.但是在使用Entity Framework时如果你使用这些列的一个作为你的主键,你可能会在进行标识识别(identity resol

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

提示26. 怎样避免使用不完整(Stub)实体进行数据库查询 什么是不完整(Stub)实体? 不完整实体是一个部分填充实体,用于替代真实的对象. 例如: 1 Category c = new Category {ID = 5}; 就是一个不完整实体. 这个实体中只有ID被填充,表示这是一个代表Category 5的Stub. Stub实体什么时候有用? 当你真正不需要知道一个实体的一切对象时,Stub实体就很有用,主要因为通过使用这种实体你可以避免不必要的查询,但也因为它们比EntityKey更

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

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

提示51. 怎样由任意形式的流中加载EF元数据 在提示45中我展示了怎样在运行时生成一个连接字符串,这相当漂亮. 其问题在于它依赖于元数据文件(.csdl .ssdl .msl)存在于本地磁盘上. 但是如果这些文件存在于web服务器中或者类似的位置,甚至你无权访本地文件系统而无法把它们拷贝到本地呢? 原来你也可以由流中加载元数据,这篇提示将告诉你怎么做. 步骤1:获得用于CSDL,MSL与SSDL的XmlTextReaders: 这可以尽可能的简单,如'new XmlTextReader(url

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

提示46. 怎样使用Code-Only排除一个属性  这次是一个真正简单的问题,由StackOverflow上这个问题引出.  问题:  当我们使用Code-Only把一个类的信息告诉Entity Framework,默认情况下每个属性会成为Entity的一部分,并作为一个存储于数据库中的结果. 通常这是你想要的结果. 但是也有例外,考虑这个类: 1 public class Person{ 2 public int ID {get;set;} 3 public string Firstname

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

提示37. 怎样进行按条件包含(Conditional Include) 问题 几天前有人在StackOverflow上询问怎样进行按条件包含. 他们打算查询一些实体(比方说Movies),并且希望预先加载一个相关项目(比方说,Reviews),但又仅要那些匹配一些条件的reviews(如,Review.Stars==5). 不幸的是EF的预先加载对此没有完整的支持,如,对于 ObjectQuery<Movie>.Include(…) 方法,Include或者是全部加载或者是不加载任何东西.

Entity Framework技巧系列之二 - Tip 6 - 8

提示6. 如何及何时使用贪婪加载 什么时候你需要使用贪婪加载? 通常在你的程序中你知道对查询到的实体将要进行怎样的操作. 例如,如果你查询一个订单以便为一个客户重新打印,你知道没有组成订单的项目即产品的信息重打印将是不完整的,所以你知道你将需要同时加载这些信息. 这是贪婪加载起作用的一类场景. 如果你知道你需要额外信息,或实体,你可能也会预先加载这些实体(贪婪加载),因为这将省下生在将来的查询. 怎样进行贪婪加载? 与一些普遍存在的错误观念相反,Entity Framework中贪婪加载即可行也

Entity Framework技巧系列之十 - Tip 37 - 41

提示37. 怎样进行按条件包含(Conditional Include) 问题 几天前有人在StackOverflow上询问怎样进行按条件包含. 他们打算查询一些实体(比方说Movies),并且希望预先加载一个相关项目(比方说,Reviews),但又仅要那些匹配一些条件的reviews(如,Review.Stars==5). 不幸的是EF的预先加载对此没有完整的支持,如,对于 ObjectQuery<Movie>.Include(-) 方法,Include或者是全部加载或者是不加载任何东西.