(翻译)Entity Framework技巧系列之六 - Tip 20 – 25
提示20. 怎样处理固定长度的主键
这是正在进行中的Entity Framework提示系列的第20篇。
固定长度字段填充:
如果你的数据库中有一个固定长度的列,例如像NCHAR(10)类型的列,当你进行一次插入时,填充会自动发生。所以例如如果你插入'12345',你将得到5个自动填充的空格,来创建一个10个字符长度的字符串。
大多数情况下,这种自动填充不会有问题。但是在使用Entity Framework时如果你使用这些列的一个作为你的主键,你可能会在进行标识识别(identity resolution)时遇到麻烦。
什么是标识识别(identity resolution)呢?
标识识别是大部分ORM支持一个重要特性。它确保对应数据库中每一个"实体"在内存中都只有一个对象。
所以如果在ObjectContext已经有一个表示一个特定"实体"的对象,这时你执行另一个查询来请求这个"实体",这个查询将返回那个已存在的相同的对象,而不是创建另一个新的对象。
这是很重要的,因为这会阻止你对相同的"实体"的不同对象进行多重矛盾的修改从而进入错误的情况。
所以为什么会出错呢?
对你来说一般不太可能会出错,但是有出错的可能。下面是原因…
在Entity Framework中我们基于CLR中的EntityKey(又名主键)的值进行"标识识别"并且我们不会为你进行任何自动增量,这点不像数据库。
这意味着如果当Product实体的主键为NCHAR(10)类型的ProductCode时,你编写下面这样的代码:
1 Product p1= new Product 2 { 3 ProductCode = "SG500", 4 Description = "7200rpm 500 GB HDD" 5 }; 6 ctx.AddToProducts(p1); 7 ctx.SaveChanges();
内存中EntityKey为'SG500'。但数据库中存储的是'SG500 '。
现在如果你编写一个这样的查询:
1 Product p2 = ctx.Products.First(p => p.ProductCode == "SG500");
由于SQLServer的查询语义,这将匹配数据库中'SG500 '这条记录,并且结果'实体'将有一个值为'SG500 '的EntityKey。
如果在相同的ObjectContext中再次运行这个查询,标识识别将发生作用,并且返回p1而不是创建一个完整的新对象。如,这个测试将不会失败:
1 Debug.Assert(Debug.ReferenceEquals(p1, p2));
不幸的是,事情不是这样的,因为p1的EntityKey为'SG500'而p2的EntityKey为'SG500 '。当使用CLR语义比较时,它们显然是不同的。所以标识识别会失败,最终你会得到不同的对象。
这时你会开始了解到你不太可能会遇到这种情况。只有你在同一个context中创建了一个实体的对象并在过后查询且修改了相同实体的这个不同对象才会真正产生问题,如果你做了这些大概是意味着你有一个长生命周期的ObjectContext,而这一般是我们不推荐的做法。
不过这整篇的描述的不太可能会出现的问题如果发生会导致极严重的错误,所以最好是提前避免。
保证你避免标识识别的问题:
所有针对这个问题的解决方案都极为简单。当你第一次创建(或附加)你的实体时简单的自行填充主键。得到如下面这样的实体:
1 Product p1= new Product 2 { 3 ProductCode = "SG500 ", 4 Description = "7200rpm 500 GB HDD" 5 };
你可能甚至会想创建一个小的工具方法来为你进行填充:
1 ProductCode = Pad("SG500",10);
如果你这样做,Entity Framework的标识识别代码会很好的工作。
如果你使用Entity Frameork时存在固定长度的主键,我强烈推荐像这样保护性代码。
提示21. 怎样使用Single()运算符 – 仅限EF4.0
这是正在进行中的Entity Framework提示系列的第21篇,是第一篇仅针对EF4.0的。
Entity Framework 4.0 Beta 1
你可能已经听说VS 2010 Beta 1已经提供给订阅者下载了,这样一部人也就可以亲身体验EF 4.0 Beta 1了。如果你对我们新的预发布的产品有任何问题,请到这个论坛。
支持Single()与SingleOrDefault()方法
我们添加的其中一个特性是对LINQ的Single()运算符的支持。
这个方法预期的语义是如果不是恰有一个匹配其将抛出一个异常。例如这样:
1 var person = (from p in ctx.People 2 where p.Firstname == “Alex” 3 select p).Single();
如果没有匹配或有多于一个匹配被找到两种情况都将抛出一个异常。
我们也添加了对SingleOrDefault()的支持。其有些微小的不同,其意思是至少应该有一个匹配,这意味着如果没有一个匹配也可以。
这是怎样实现的
论坛中一些人已经注意到我们已经添加了对 Single() 的支持,并且注意到被执行的T-SQL是一个 TOP(2) 的查询。就如下面所示这样:
1 SELECT TOP (2) … 2 FROM [dbo].[People] AS [Extent1] 3 WHERE N'Alex' = [Extent1].[Firstname]
正如论坛中Matthieu在他答案中提到的,如果你仔细考虑这个语句,会发现其很有意义。我们不得不得到前两条结果(不需要更多)来判断我们是否应该抛出一个异常,如果结果的DataReader有两行而不是仅仅一行,异常就被抛出了。
提示22. 怎样使Include真正的Include
这是正在进行中的Entity Framework提示系列的第22篇
如果你想要在Entity Framework中进行预先加载,一般来说非常简单,你仅需编写像这样的代码:
1 var results = 2 from post in ctx.Posts.Include(“Comments”) 3 where post.Author.EmailAddress == “alexj@microsoft.com” 4 select post;
在这个例子中,针对每一个匹配的文章你将得到其评论,这些都在一个查询中,又称预先加载。
酷。到目前为止很好。
然而如果你开始做更多改变查询"形状"的有趣的查询,或者引入一个join或其它东西,可能像如下这样:
1 var results = 2 from post in ctx.Posts.Include(“Comments”) 3 from blog in post.Blogs 4 where blog.Owner.EmailAddress == “alexj@microsoft.com” 5 select post;
这个include将不在工作。
为什么?
当第一个例子被翻译为一个查询,被选择的列自始至终都是相同的。过滤条件引入一个join,但是这不影响哪些列被选择,所以从 Include() 执行开始到最后select查询的形状是不变的。
在第二例子中,当 Include()被执行时查询仅包含post表的列,但是接下来第二个from改变了查询的形状以将post表与blog表两个表的列包含在内,这也就是说结果的形状改变了,虽然是临时的,但这个临时的形状改变使 Include()停止工作。
目前在这个特定的例子中,你可以像下面这样重写这个查询,来使include再次工作:
1 var results = 2 from post in ctx.Posts.Include(“Comments”) 3 where post.Blogs.Any( 4 b => b.Owner.EmailAddress == “alexj@microsoft.com” 5 ) 6 select post;
…这样include将再次工作。因为查询已经被重写以避免在 Include() 被应用起直到查询结束这个过程中改变形状。
不幸的是这种类型的变通方案虽然会有效果但具有一些入侵性,因为其强制你改变编写查询的方式而仅为了可以使include工作。
然而有一种更好的选项。
变通方案
这个变通方案实际上非常简单,你仅需将Include移动到查询的尾部。
1 var results = 2 ((from post in ctx.Posts 3 from blog in post.Blogs 4 where blog.Owner.EmailAddress == “alexj@microsoft.com” 5 select post) as ObjectQuery<Post>).Include(“Comments”);
为了让这个可以工作,最后一个select必须为实体,换句话说需要为 select post 而不是 select new {…} ,如果这样你可以将结果的类型转换为ObjectQuery,并进行include。
这可以很好的工作,因为在include被应用起直到查询结束这个过程中改变保持不变。
提示23. 怎样在EF4中伪造Enums
当前EF4中还不支持Enums。
现在我们将倾听对于Beta1的反馈,来进行一些调整,所以你很难预料,但是此时其看起来不会被支持。
昨天我带来一个变通方案,虽然只是很少的工作,但相当有趣。
变通方案
要使这种方式工作,你需要使用.NET4.0且需要使用POCO类。
想象你有一个下面这样的枚举:
1 public enum Priority 2 { 3 High, 4 Medium, 5 Low 6 }
第一步是创建一个仅有一个属性的ComplexType,如下面这样的东西:
1 <ComplexType Name="PriorityWrapper" > 2 <Property Type="Int32" Name="Value" Nullable="false" /> 3 </ComplexType>
然后如果你想要在一个Entity中有一个返回一个Enum替代返回包装的ComplexType的属性。
正如我所说,这仅在POCO模式下工作。这是因为你需要在PriorityWrapper这个复合类型中做一些有趣的事:
1 public class PriorityWrapper 2 { 3 private Priority _t; 4 public int Value { 5 get { 6 return (int) _t; 7 } 8 set { 9 _t = (Priority) value; 10 } 11 } 12 public Priority EnumValue 13 { 14 get { 15 return _t; 16 } 17 set { 18 _t = value; 19 } 20 } 21 }
注意这个类型有一个int类型的Value属性正如ComplexType的定义,但是这个类也有一种方式通过EnumValue属性设置与获取Priority。
有了这个类我们可以在POCO实体中使用它,所以例如想象你有一个Task实体:
1 public class Task 2 { 3 public virtual int Id { get; set; } 4 public virtual PriorityWrapper Priority { get; set; } 5 public virtual string Title{ get; set;} 6 }
下一步挺有趣,在PriorityWrapper与Priority之间添加一些隐式转换:
1 public static implicit operator PriorityWrapper(Priority p) 2 { 3 return new PriorityWrapper { EnumValue = p }; 4 } 5 6 public static implicit operator Priority(PriorityWrapper pw) 7 { 8 if (pw == null) return Priority.High; 9 else return pw.EnumValue; 10 }
用上这些隐式转换,你会有一种错觉,Task类中的Priority属性(译注:PriorityWrapper类型)实际上就是一个Priority(译注:Enum类型)。
例如你可以这样做:
1 Task task = new Task { 2 Id = 5, 3 Priority = Priority.High, 4 Title = “Write Tip 23” 5 };
而不需要这样做:
1 Task task = new Task { 2 Id = 5, 3 Priority = new PriorityWrapper {EnumValue = Priority.High }, 4 Title = “Write Tip 23” 5 };
可以这样:
1 if (task.Priority == Priority.High)
而不必:
1 if (task.Priority.EnumValue == Priority.High)
但是查询中如何呢?
你甚至可以在查询中使用这个枚举:
1 var highPriority = 2 from task in ctx.Task 3 where task.Priority.Value == (int) Priority.High 4 select task;
很酷吧?
目前这与我们原生支持枚举相比还不够好,但是也差的不多,尤其是从一些与实体相对的编程的观点来说。
编码愉快。
提示24. 怎样由一个实体得到ObjectContext
客户经常问到怎样由一个Entity返回到ObjectContext。
现在一般来说我们不推荐进行这种尝试。但是有时候你确实需要一种方式来取得ObjectContext。
例如如果你在一个方法中,当前作用域只能访问到Entity,你需要ObjectContext,可能用于进行一些其它查询等等。
一种想法是改变方法的签名,这样当方法被调用时调用者必须传入ObjectContext。不幸的是像那样改变签名并不总是可行的,尤其是如果你正实现一些具体实现无关的持久化接口,如一个Repository,另外也有可能引起你全部代码的级联改变,讨厌。
另一个想法是将ObjectContext放入一个线程本地变量。类似于 HttpContext.Current 的工作方式。
但是这两种解决方案可能都不适合你…
警告:
在进入解决方案之前,先顺序给出一系列重要的警告:
警告#1:这个解决方案仅在Entity至少有一个关联时可用。
警告#2:这个解决方案不会证明理想的性能特征,因为虽然我们仅仅是获取Context,其也调用了若干涉及查找/计算的中间方法:
警告#3:对于未附加的实体此方法不适用。虽然令人惊讶的其可以与NoTracking查询一起工作,你可能曾认为NoTracking实体本质上是Detached的,但那严格来说是不正确的。虽然NoTracking查询不被跟踪,但关系加载仍然会工作,而这就是我们利用的东西。
警告CHECK…
你已经被警告!
变通方案:
这个解决方案依赖一个单独的扩展方法:
1 public static ObjectContext GetContext( 2 this IEntityWithRelationships entity 3 ) 4 { 5 if (entity == null) 6 throw new ArgumentNullException("entity"); 7 8 var relationshipManager = entity.RelationshipManager; 9 10 var relatedEnd = relationshipManager.GetAllRelatedEnds() 11 .FirstOrDefault(); 12 13 if (relatedEnd == null) 14 throw new Exception("No relationships found"); 15 16 var query = relatedEnd.CreateSourceQuery() as ObjectQuery; 17 18 if (query == null) 19 throw new Exception("The Entity is Detached"); 20 21 return query.Context; 22 }
这是怎样工作的呢?
在.NET 3.5中所有含有关联的实体实现了IEntityWithRelationships接口,事实上甚至在EF4中如果你有一个ChangeTracking代理用于POCO实体,这个proxy也实现IEntityWithRelationships接口。
由那你可以访问到RelationshipManager并获取第一个RelatedEnd,这跟我们选择哪个end无关,因为我们不关心实际的end,这仅是一种取得end的手段。
由RelateEnd,你创建一个OjbectOuery,但不执行,来加载相关的实体。如果你得到null是因为Entity由ObjectContext中分离了,就像我在警告中说的,你不太走运。
最后由查询中你可以得到ObjectContext。
用上这个扩展方法,你只需简单的这样做:
1 ObjectContext context = myEntity.GetContext();
或许你想要强类型的Context…这也不成问题:
1 MyContext context = myEntity.GetContext() as MyContext;
小事一桩,很容易!
提示25. 怎样通过主键很容易的得到Entity
有时候,你更想编写这样的代码:
1 var customer = ctx.Customers.GetCustomerById(5);
而不是像这样:
1 var customer = ctx.Customers.First(c => c.ID == 5);
在.NET 4.0中,通过修改完成代码生成的T4模版实现这个目的是小事一桩。你只需为每个EntityType新增一个扩展方法,如下面这样:
1 public static Customer GetCustomerById( 2 this ObjectQuery<T> query, 3 int Id 4 ) 5 { 6 return query.First(c => c.ID == Id); 7 }
这很好,但是有没有一种方法在不为每个类型添加一个方法的情况下实现这个目的呢?
通用解决方案
的确有一个更通用的方法,虽不是类型安全的,然而它可以很出色的工作。
这种思想的核心是使用eSQL构建一个查询。此方法可同时用于3.5与4.0中:
1 public static T Get<T, K>(this ObjectQuery<T> query, K key) 2 { 3 //Get the EntityType 4 EntityType entityType = query.Context.MetadataWorkspace 5 .GetCSpaceEntityType<T>(); 6 7 if (entityType.KeyMembers.Count != 1) 8 throw new Exception("You need to pass all the keys"); 9 10 //Build the ESQL 11 string eSQL = string.Format("it.{0} = @{0}", 12 entityType.KeyMembers[0].Name); 13 14 //Execute the query 15 return query.Where( 16 eSQL, 17 new ObjectParameter(entityType.KeyMembers[0].Name, key) 18 ).First(); 19 }
这是怎样工作的呢?
-
此方法使用了提示13中的GetCSpaceEntityType<T>()这个扩展方法,来获取用于T的EntityType。
-
一但我们得到EntityType,我们检查并确保我们有正确数目的参数,也就是说,如果这个方法只接收一个key值,那么要验证的EntityType必须只有一个单一的主键列。
-
接下来我们生成一个eSQL片段来修改查询的where子句。例如,如果你正查找一个客户,主键为ID,我们将生成"it.ID = @ID"。
-
下一步我们创建一个ObjectParameter用于上一步的eSQL中引用的参数,参数的值来自此方法的名为key的参数。
-
最后我们执行这个查询并得到First()的结果。在4.0中你应该使用Single()来代替,但是3.5中不支持这个方法。
这样就完成了。
现在你可以这样写:
1 Customer c = ctx.Customers.Get(5);
现在需要一句警告,这个方法不是类型安全的,所以下面代码会编译通过:
1 Customer c = ctx.Customers.Get("Some Random String");
即使Customer有一个整型的主键。你将只会在运行时当eSQL被解析时注意到错误。
这个实现不支持联合主键,但是添加更多接收更多参数的重载是相当容易的。
这个就留给读者作为练习了!