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

时间:2021-09-24 19:27:19

提示26. 怎样避免使用不完整(Stub)实体进行数据库查询

什么是不完整(Stub)实体?

不完整实体是一个部分填充实体,用于替代真实的对象。

例如:

1 Category c = new Category {ID = 5};

就是一个不完整实体。

这个实体中只有ID被填充,表示这是一个代表Category 5的Stub。

Stub实体什么时候有用?

当你真正不需要知道一个实体的一切对象时,Stub实体就很有用,主要因为通过使用这种实体你可以避免不必要的查询,但也因为它们比EntityKey更容易使用。

下面是一些例子:

场景1 – 使用Stub实体构建一个关联:

这可能是Stub实体最常见的使用,让我们假设你想要在一个新产品与一个已存在的类别间构建一个关联,如果你希望的已存在类别为ID=5的那个,默认情况下你会这样做:

1 Product p = new Product {
2 Name = “Bovril”,
3 Category = ctx.Categories.First(c => c.ID == 5)
4 };
5 ctx.AddToProducts(p);
6 ctx.SaveChanges();

但这样会对Category执行一次数据库查询,如果你仅是构建一个关联这就是多余的。你不需要整个实体,你已经知道所有需要的(即ID),所以你可以使用一个Stub实体重写以上代码:

Entity Framework技巧系列之七 - Tip 26 – 28
 1 Category category = new Category { ID = 5};
2 //如果你不想这里出现字符串,参考提示13与16!
3 ctx.AttachTo(“Categories”,category);
4
5 Product product = new Product {
6 Name = “Bovril”,
7 Category = category
8 };
9 ctx.AddToProducts(product);
10 ctx.SaveChanges();
Entity Framework技巧系列之七 - Tip 26 – 28

这样你就省下一次数据库查询。

注意:你也可以使用这个方法通过集合(collections)来构建新的关联(relationships)。

场景2 – 使用Stub实体进行删除:

进行删除的标准方法如下:

1 Category category = ctx.Categories.First(c => c.ID == 5);
2 ctx.DeleteObject(category);
3 ctx.SaveChanges();

第一行是一个查询,来获得一个"完整"实体,但是如果这个实体与其它实体没有关联则可以使用一个简单的stub来进行删除:

1 Category category = new Category { ID = 5 };
2 ctx.AttachTo(“Categories”,category);
3 ctx.DeleteObject(category);
4 ctx.SaveChanges();

再次,通过使用stub省下了一次查询。

场景3 – 删除一个Stub实体及其关联

然而如果你想要删除的实体有一个关联(如一个产品有一个类别),Entity Framework需要知道有关关联的一些以便进行删除。如果你执行查询来获得你要删除的实体,EF会使用称作relationship span的特性自动获取额外的信息。

但是又一次我们想省下查询,通过使用stub实体我们可以告诉EF所使用的关联,正如你猜测,另一个stub实体,如下:

Entity Framework技巧系列之七 - Tip 26 – 28
1 Product product = new Product {
2 ID = 5,
3 Category = new Category { ID = 5 }
4 };
5 ctx.AttachTo(“Products”,product);
6 ctx.DeleteObject(product);
7 ctx.SaveChanges();
Entity Framework技巧系列之七 - Tip 26 – 28

这里通过使用2个stub实体,我们再次省下一次查询

*在.NET4.0中,你使用FK associations,以上方法不再正确。Entity Framework无需了解关联就可以恰当的进行删除。就如Roger Jennings将会说hoorah。

场景4 – 删除含有时间戳的实体

如果一个实体有一个用于并发标识的列,通常是一个时间戳,这样当你创建stub时,你也需要提供提供那个值:

Entity Framework技巧系列之七 - Tip 26 – 28
1 Order order = new Order{
2 OrderNo = 3425,
3 Timestamp = timestamp,
4 Customer = new Customer { ID = 7}
5 };
6 ctx.AttachTo(“Orders”, order);
7 ctx.DeleteObject(order);
Entity Framework技巧系列之七 - Tip 26 – 28

使用stub,节省一次查询。

场景5 – 更新一个实体

如果你要更新一个实体,你仅需附加一个表示实体原始(original)版本的东西,stub再次登场了:

Entity Framework技巧系列之七 - Tip 26 – 28
1 Person person = new Person{
2 ID = 65,
3 Firstname = “Jo”,
4 Surname = “Andrews”
5 };
6 ctx.AttachTo(“People”, person);
7 person.Surname = “James”; // Yes my wife took my surname!
8 ctx.SaveChanges();
Entity Framework技巧系列之七 - Tip 26 – 28

使用stub,节省一次查询!

摘要

5个场景,节省5类查询,所以正如你所见,Stub实体相当有用!

它们不仅节省了数据库查询,同时是你的应用性能与可扩展性更好。它们也使你的代码相较EntityKey写的版本更可读。

一般使用它们的方式也很简单:

1. 构造一个包含你需要的字段的stub实体。

2. 附加这个stub实体

3. 执行你需要做的(构建关联,删除,更新等)

如果有问题告诉我。

提示27. 怎样在保存前执行验证

在将数据保存到数据库前验证你的实体是'可用'的是常见的需要。

一种初级的验证形式是试图保证在实体添加到context中前是可用的,但是如果你传入无效状态这种方法不会有帮助,例如当你创建一个图表,但是更重要的是这种初级方法对update完全无效。

你真正需要的是一个可以在你调用 SaveChanges() 时告诉你,你将尝试保存的东西的函数,这样你可以在恰当的时间执行验证。

怎样做呢?

首先我们需要在自定义的Context中添加一个 Validate() 方法,其通过ObjectStateManager来验证所有实体。

可以通过在Context(的一个部分类)中添加一个如下这样的Validate方法来实现:

Entity Framework技巧系列之七 - Tip 26 – 28
 1 public void Validate()
2 {
3 var stateEntries = ObjectStateManager.GetObjectStateEntries(
4 EntityState.Added |
5 EntityState.Modified |
6 EntityState.Deleted )
7 .Where(e => e.Entity is IValidingEntity);
8 foreach (var stateEntry in stateEntries)
9 {
10 var entity = stateEntry.Entity as IValidingEntity;
11 entity.Validate(stateEntry.State);
12 }
13 }
Entity Framework技巧系列之七 - Tip 26 – 28

正如你所见这段代码查找所有的ObjectState为Added,Modified或Deleted状态的实体。我们忽略Detached与unchanged状态的实体,因为它们还没改变!

如果这些实体实现了IValidatingEntity接口,我们调用这些实体的验证方法并传入实体状态(在不同状态下验证逻辑是不同的,正如你将在下面看到的例子)。

IValidatingEntity的定义非常简单,看起来如下:

1 public interface IValidingEntity
2 {
3 void Validate(EntityState state);
4 }

现在我们仅需让所有想要验证的类实现实现这个接口。这非常简单仅需添加一个部分类。

这个例子提供了对Accout Transfer实体的验证逻辑:

Entity Framework技巧系列之七 - Tip 26 – 28
 1 public partial class AccountTransfer: IValidatingEntity
2 {
3 public void Validate(EntityState state)
4 {
5 if (state == EntityState.Added)
6 {
7 if (this.Amount <= 0)
8 throw new InvalidOperationException(“Transfers must have a non-zero positive value”);
9
10 if (this.SourceAccount == null)
11 throw new InvalidOperationException(“Transfers require a source account”);
12 if (this.TargetAccount == null)
13 throw new InvalidOperationException(“Transfers require a target account”);
14
15 if (this.SourceAccount == this.TargetAccount)
16 throw new InvalidOperationException(“Transfers must be between two different accounts”);
17 }
18 else if (state == EntityState.Modified)
19 {
20 throw new InvalidOperationException(“Modifying a transfer is not allowed, make a correcting transfer instead”);
21 }
22 else if (state == EntityState.Deleted)
23 {
24 throw new InvalidOperationException(“Deleting a transfer is not allowed, make a correcting transfer instead”);
25
26 }
27 }
28 }
Entity Framework技巧系列之七 - Tip 26 – 28

现在你可以在Context中像这样进行验证:

1 using (BankingContext ctx = new BankingContext())
2 {
3 … // Random activities
4 ctx.Validate();
5 ctx.SaveChanges();
6 }

但是这样很容易忘记调用 Validate() ,所以如果可以让验证作为 SaveChanges() 的一部分发生会更好;

其实这也不难,你只需重写 OnContextCreated() 这个部分方法,在其中为SavingChanges事件订阅一个处理函数,并在这个事件处理函数中调用 Validate() 方法,像这样:

Entity Framework技巧系列之七 - Tip 26 – 28
1 partial void OnContextCreated()
2 {
3 this.SavingChanges += new EventHandler(BankingContext_SavingChanges);
4 }
5 void BankingContext_SavingChanges(object sender, EventArgs e)
6 {
7 Validate();
8 }
Entity Framework技巧系列之七 - Tip 26 – 28

现在当你调用 SaveChanges() 时验证会自动发生。

很酷吧。

这段代码同时适用于3.5与4.0,但是在4.0你可以让它更好。

事实上Danny Simmons在TechEd上演示了这一点,通过自定义一个T4模版让这些代码直接加入到Context与Entities中。他让Entities上的Validate方法调用一个部分方法,所以默认情况下调用Validate不会发生什么,但是如果你需要验证,你只需添加向部分类中添加一个部分方法。

你甚至可以也为3.5写一个T4模版来做同样的事情。

无论如何这里有一大些选择,好好享受!

警告:

对于像验证一样复杂的问题,总是有一些警告!

所以请注意如果你验证逻辑的一部分做了一些额外的更改或增加,这些新的更改将不会被验证。这是因为有更改的实体的列表在这些改变发生前已经"确定"了。

一般情况下这不是问题,因为你总是希望你的验证逻辑将对象变为可用状态:)

但是请了解这些限制。

提示28. 怎样实现预先加载策略

背景:

过去2年中很多人抱怨Entity Framework中预先加载的工作方式,更准确的说你要求Entity Framework进行预先加载的方式。

如下是现在的做法:

1 var results = from b in ctx.Blogs.Include(“Posts”)
2 where b.Owner == “Alex”
3 select b;

这个代码段要求EF预先加载与每一篇Blog匹配的Post,并且这工作的很好。

问题是"Posts"这个字符串。普通的LINQ特别是LINQ to SQL已经宠坏我们,现在我们在任何地方都希望得到类型安全,而字符串..不是类型安全的。

取而代之每个人希望代码如下面这样:

1 var results = from b in ctx.Blogs.Include(b => b.Posts)
2 where b.Owner == “Alex”
3 select b;

这更安全一些。并且很多人之前已经尝试过这样做,包括我的同事Matthieu。

但是下面这样会更好:

1 var strategy = new IncludeStrategy<Blog>();
2 strategy.Include(b => b.Owner);
3 var results = from b in strategy.ApplyTo(ctx.Blogs)
4 where b.Owner == “Alex”
5 select b;

因为这里你可以在查询间重新使用策略。

设计目标:

所以我决定自娱自乐一下来扩展这种想法以支持这些策略。

下面是我想要支持的东西:

1 var strategy = Strategy.NewStrategy<Blog>();
2 strategy.Include(b => b.Owner)
3 .Include(p => p.Comments); //包含子对象
4 strategy.Include(b => b.Posts); //包含多个

基于策略类型生成子类型的能力

Entity Framework技巧系列之七 - Tip 26 – 28
1 public class BlogFetchStrategy: IncludeStrategy<Blog>
2 {
3 public BlogFetchStrategy()
4 {
5 this.Include(b => b.Owner);
6 this.Include(b => b.Posts);
7 }
8 }
Entity Framework技巧系列之七 - Tip 26 – 28

这样你进行如下操作:

1 var results = from b in new BlogFetchStrategy().ApplyTo(ctx.Blogs)
2 where b.Owner == “Alex”
3 select b;

实现

如下是我的实现方法:

1) 创建IncludeStrategy<T>类:

Entity Framework技巧系列之七 - Tip 26 – 28
 1 public class IncludeStrategy<TEntity>
2 where TEntity : class, IEntityWithRelationships
3 {
4 private List<string> _includes = new List<string>();
5 public SubInclude<TNavProp> Include<TNavProp>(
6 Expression<Func<TEntity, TNavProp>> expr
7 ) where TNavProp : class, IEntityWithRelationships
8 {
9 return new SubInclude<TNavProp>(
10 _includes.Add,
11 new IncludeExpressionVisitor(expr).NavigationProperty
12 );
13 }
14 public SubInclude<TNavProp> Include<TNavProp>(
15 Expression<Func<TEntity, EntityCollection<TNavProp>>> expr
16 ) where TNavProp : class, IEntityWithRelationships
17 {
18 return new SubInclude<TNavProp>(
19 _includes.Add,
20 new IncludeExpressionVisitor(expr).NavigationProperty
21 );
22 }
23 public ObjectQuery<TEntity> ApplyTo(ObjectQuery<TEntity> query)
24 {
25 var localQuery = query;
26 foreach (var include in _includes)
27 {
28 localQuery = localQuery.Include(include);
29 }
30 return localQuery;
31 }
32 }
Entity Framework技巧系列之七 - Tip 26 – 28

注意有一系列东西含有我们想要Include的对象。同时注意 ApplyTo(…) 方法允许你将Includes注册到一个ObjectQuery<T>,只要T匹配。

但是当然大部分工作集中于2个 Include(..) 方法。

有两个include是因为我想让其中一个包含引用(References)另一个来包含集合(Collections)。这个实现被设计为工作于.NET3.5SP1,所以我可以依赖于那些拥有实现了IEntityWithRelationships接口的关系(Include也只对这些类型起作用)的类。这也是使用了泛型约束的原因。

一个很有趣的地方是针对为包括Collections的 Include() 方法,虽然Expression为Expression<Func<TEntity, EntityCollection<TNavProp>>> ,用于创建子包含的返回对象被定义为TNavProp类型。这允许我们如下这样灵活的绕过需求来解释表达式:

1 Include(b => b.Posts.SelectMany(p => p.Author));

或者创造一些如下这样DSL类型的代码:

1 Include(b => b.Posts.And().Author);

来取代下面这样的代码:

1 Include(b => b.Posts).Include(p => p.Author);

前者更容易实现,并且我也有这样做的理由。

对于整个设计想法是主要的。

2) IncludeExpressionVisitor是继承自ExpressionVisitor例子的副本(你可以在这里找到)的一个类。它非常简单,事实上这非常简单甚至可能在这里使用visitor有点过度,但是我打算专门研究正确的模式等:

Entity Framework技巧系列之七 - Tip 26 – 28
 1 public class IncludeExpressionVisitor : ExpressionVisitor
2 {
3 private string _navigationProperty = null;
4 public IncludeExpressionVisitor(Expression expr)
5 {
6 base.Visit(expr);
7 }
8 public string NavigationProperty
9 {
10 get { return _navigationProperty; }
11 }
12 protected override Expression VisitMemberAccess(
13 MemberExpression m
14 )
15 {
16 PropertyInfo pinfo = m.Member as PropertyInfo;
17 if (pinfo == null)
18 throw new Exception(
19 "You can only include Properties");
20
21 if (m.Expression.NodeType != ExpressionType.Parameter)
22 throw new Exception(
23 "You can only include Properties of the Expression Parameter");
24
25 _navigationProperty = pinfo.Name;
26 return m;
27 }
28 protected override Expression Visit(Expression exp)
29 {
30 if (exp == null)
31 return exp;
32 switch (exp.NodeType)
33 {
34 case ExpressionType.MemberAccess:
35 return this.VisitMemberAccess(
36 (MemberExpression)exp
37 );
38 case ExpressionType.Lambda:
39 return this.VisitLambda((LambdaExpression)exp);
40 default:
41 throw new InvalidOperationException(
42 "Unsupported Expression");
43 }
44 }
45 }
Entity Framework技巧系列之七 - Tip 26 – 28

正如你所见,这个visitor相当首限制,它只识别LambdaExpression与MemberExpression。当访问一个MemberExpression时它检查来确保被访问的Member是一个属性,并且member被直接绑定到参数(例如,p.Property是好的但p.Property.SubProperty却不是)。一旦符合约束它记下NavigationProperty的名称。

3) 之前我们知道NavigationProperty命名了IncludeStrategy。Include方法创建一个SubInclude<T>对象。其负责将我们想要的东西注册来包含NavigationProperty,并提供一种机制来串联更多的子inlcude。

这个SubInclude<T>类如下:

Entity Framework技巧系列之七 - Tip 26 – 28
 1 public class SubInclude<TNavProp>
2 where TNavProp : class, IEntityWithRelationships
3 {
4 private Action<string> _callBack;
5 private string[] _paths;
6
7 internal SubInclude(Action<string> callBack, params string[] path)
8 {
9 _callBack = callBack;
10 _paths = path;
11 _callBack(string.Join(".", _paths));
12 }
13 public SubInclude<TNextNavProp> Include<TNextNavProp>(
14 Expression<Func<TNavProp, TNextNavProp>> expr
15 ) where TNextNavProp : class, IEntityWithRelationships
16 {
17 string[] allpaths = _paths.Append(
18 new IncludeExpressionVisitor(expr).NavigationProperty
19 );
20
21 return new SubInclude<TNextNavProp>(_callBack, allpaths);
22 }
23 public SubInclude<TNextNavProp> Include<TNextNavProp>(
24 Expression<Func<TNavProp, EntityCollection<TNextNavProp>>> expr
25 ) where TNextNavProp : class, IEntityWithRelationships
26 {
27 string[] allpaths = _paths.Append(
28 new IncludeExpressionVisitor(expr).NavigationProperty
29 );
30
31 return new SubInclude<TNextNavProp>(_callBack, allpaths);
32 }
33 }
Entity Framework技巧系列之七 - Tip 26 – 28

4) 现在唯一缺少是一个将另一个元素附加到一个数组的扩展方法,它看起来如下:

1 public static T[] Append<T>(this T[] initial, T additional)
2 {
3 List<T> list = new List<T>(initial);
4 list.Add(additional);
5 return list.ToArray();
6 }

使用这些代码你可以很容易的编写自己的预先加载策略类,只需派生自IncludeStrategy<T>类。

所有你需要的代码都在这篇文章中,但请记住这仅是一个示例,它不是微软官方发行版,正是如此也没有严格的测试过等。

如果你接受我仅是一个项目经理,并且我明显易出错的事实,而你仍然打算尝试这段代码,你可以在这里下载源码的一份拷贝。

祝你愉快。