如何深入掌握Entity Framework 4.1 Code First的进阶技巧?

2026-05-27 05:262阅读0评论SEO教程
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1957个文字,预计阅读时间需要8分钟。

如何深入掌握Entity Framework 4.1 Code First的进阶技巧?

写系列篇的上一篇已经是很久以前的事情了==在此期间,EF 4.1的RTW已经发布,NH 3.2的Alpha也出了第二版。其实不是我不懒,工作中一直在使用EF 4.1。主要是上次承诺的一个Update功能还没搞定。

写系列的上一篇已经是很久之前的事儿了= =在此期间,EF 4.1的RTW都已经出来了,NH 3.2的Alpha已经2了。。。其实不是我懒,工作中也在一直使用EF 4.1。主要是上次承诺过的一个Update功能搞不定= =

总之这一次的目标是

  • 实现一个完整的IRepository(添加增删改能力)
  • 领域对象的继承
  • 事物

首先来看IRepository

我的接口如下

1: public interface IRepository<TEntity>

2: where TEntity : IEntity

3: {

4: IEnumerable<TEntity> FindAll();

5: TEntity FindById(int id);

6: void Add(TEntity entity);

7: void Delete(TEntity entity);

8: void Update(TEntity entity);

9: }

应该算是一个最基本的仓储接口了。

其中前几个接口都是很好实现的,上次提及的DbSet对象提供了相应的接口,直接调用即可,代码是类似这样的。

1: protected DbSet<TEntity> DbSet

2: {

3: get { return m_dbContext.Set<TEntity>(); }

4: }

5:

6: public IEnumerable<TEntity> FindAll()

7: {

8: return DbSet;

9: }

10:

11: public TEntity FindById(int id)

12: {

13: return DbSet.SingleOrDefault(entity => entity.Id == id);

14: }

15:

16: public void Add(TEntity entity)

17: {

18: DbSet.Add(entity);

19: m_dbContext.SaveChanges();

20: }

21:

22: public void Delete(TEntity entity)

23: {

24: DbSet.Remove(entity);

25: m_dbContext.SaveChanges();

26: }

关键问题是最后的Update方法

DbSet对象并没有提供相应的接口,为什么呢?因为EF相信自己的Self Tracking能力。也就是说,EF认为把一个entity从context中加载出来,做一些变更,然后直接SaveChanges就可以了,不需要特意提供一个Update方法。

但是这里有一个前提,就是“entity是从context中加载出来”。如果entity是新new出来的呢?比如在MVC里,entity很可能是ModelBinder帮我们new出来的,context对它一无所知,直接SaveChanges显然不会有任何效果。

那么如何让context可以理解一个新new出来的entity呢?这要从EF处理entity状态开始说起。

EF定义了如下几种State(注意这个枚举是Flag)

1: [Flags]

2: public enum EntityState

3: {

4: Detached = 1,

5: Unchanged = 2,

6: Added = 4,

7: Deleted = 8,

8: Modified = 16,

9: }

其中Detached状态,就是entity还没有attach到context(实际上是Attach到某个DbSet上)的状态。具体怎么做呢?直接上代码

1: public void Update(TEntity entity)

2: {

3: var entry = m_dbContext.Entry(entity);

4: if (entry.State == EntityState.Detached)

5: {

6: //DbSet.Attach(entity);

7: entry.State = EntityState.Modified;

8: }

9: m_dbContext.SaveChanges();

10: }

可以看到上面的代码给出了两种办法,一种是直接修改entry的State,另一种是调用DbSet对象的Attach方法。

注意到DbContext.Entry方法取出的DbEntityEntry对象。利用这个对象可以做很多有用的事哦~~园子里的EF专家LingzhiSun有一篇blog,大家可以去读读。

不过这个实现有一个缺陷

我们上面谈到过,上面这个实现实际上是把entity attach到了对应的DbSet上。但是如果你的代码是类似如下的,就可能产生问题(没有亲试,感觉上是这样的= =)

1: var heros = repository.FindAll();

2: var hero = heros.First(h => h.Id == 1);

3: var heroNew = new Hero

4: {

5: Id = hero.Id,

6: Name = hero.Name,

7: Race = hero.Race

8: };

9: repository.Update(heroNew);

应该是会抛出来一个异常说“An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.”

异常说的很明白,你的DbSet已经加载过一次id为1的对象了,当试图去attach另一个id为1的对象的时候EF就会无所适从。

那是不是说刚才给出的那个实现根本就行不通呢?不是的!事实上微软官方的文章上就是采用这种方法的。关键就在于当你尝试去attach一个entity的时候,要保证DbSet还没有加载过!我们看上面那篇微软的文章里是如何保证这一点的

1: public class BlogController : Controller

2: {

3: BlogContext db = new BlogContext();

4:

5: //...

6:

7: [HttpPost]

8: public ActionResult Edit(int id, Blog blog)

9: {

10: try

11: {

12: db.Entry(blog).State = EntityState.Modified;

13: db.SaveChanges();

14:

15: return RedirectToAction("Index");

16: }

17: catch

18: {

19: return View();

20: }

21: }

22: }

很明显,在执行Edit这个Action之前,DbSet没有加载过,因为MVC帮我们保证了DbContext实例是request结束就被销毁的.

也就是说,结论是使用这种Update实现方式对context的生命周期是有要求的.当然我的例子中context的生命周期也是per-request的所以没关系。

那么如果我们想使用其他的context生命周期管理方式呢?比如希望整个application只有一个context实例?

让我们来给出另一种实现

回过头来想一想在实现Update这个方法的时候我们最初遇到的问题:entity不是从context中加载的而是直接new出来的

那么我们手动的来加载一次就好了么,代码类似于这样

1: public void Update(Hero entity)

2: {

3: var entry = m_dbContext.Entry(entity);

4: if (entry.State == EntityState.Detached)

5: {

6: Hero entityToUpdate = FindById(entity.Id);

7: entityToUpdate.Id = entity.Id;

8: entityToUpdate.Name = entity.Name;

9: entityToUpdate.Race = entity.Race;

10: }

11: m_dbContext.SaveChanges();

12: }

不过由于失去了泛型的优势,给每个domain model都要实现一个Update方法比较烦,可以用一些框架来解决这个问题,例如EmitMapper(园子里也讨论过这个东西)

1: public void Update(TEntity entity)

2: {

3: var entry = m_dbContext.Entry(entity);

4: if (entry.State == EntityState.Detached)

5: {

6: var entityToUpdate = FindById(entity.Id);

7: EmitMapper.ObjectMapperManager.DefaultInstance.GetMapper<TEntity, TEntity>().Map(entity, entityToUpdate);

8: }

9: m_dbContext.SaveChanges();

10: }

当然这个实现也有不好的地方例如说当domain里有一些跟ORM没关系的property时也会被EmitMapper改写掉。

下一个议题是领域对象的继承

让领域对象实现继承的好处是不言而喻的,可以使用到多态等OO带来的好处。相对的就对ORM提出了更高的要求。

如何深入掌握Entity Framework 4.1 Code First的进阶技巧?

我们知道映射对象树到数据库有三种经典的实现方式:Table Per Type、Table Per Hierarchy和Table Per Concrete class,这次我们来实践最简单的一种:Table Per Hierarchy。

回想我们上一次的类

1: public class Hero : IEntity

2: {

3: public int Id { get; set; }

4: public string Name { get; set; }

5: public bool IsSuperHero { get; set; }

6: public virtual Race Race { get; set; }

7: }

把它拆成两个有继承关系的类

1: public class Hero : IEntity

2: {

3: public int Id { get; set; }

4: public string Name { get; set; }

5: //public bool IsSuperHero { get; set; }

6: public virtual Race Race { get; set; }

7: }

8: public class SuperHero : Hero

9: {

10:

11: }

在EF Code First中这种单表继承的映射关系是这样来写的

1: Map<Hero>(hero => hero.Requires(ColumnNameMappingStrategy.Value.To("IsSuperHero")).HasValue(false)).ToTable(tableNameMappingStrategy.To("Hero"));

2: Map<SuperHero>(hero => hero.Requires(ColumnNameMappingStrategy.Value.To("IsSuperHero")).HasValue(true)).ToTable(tableNameMappingStrategy.To("Hero"));

另外两种方式的实现也不复杂,可以参考这里。这个实例还是CTP5的API,跟4.1最终版有些区别不过应该影响不大。

今天最后的议题是事物

可以用TransactionScope来管理,虽然看起来有些浪费,毕竟例子中不涉及Transaction传播,连DbContext都只有一个实例。代码如下

1: [HttpPost]

2: public ActionResult Edit(TEntity entity)

3: {

4: try

5: {

6: using (var scope = new TransactionScope())

7: {

8: ModelRepository.Update(entity);

9: scope.Complete();

10: }

11: return RedirectToAction("Index");

12: }

13: catch

14: {

15: return View();

16: }

17: } Spring实际上也可以用AOP的方式管理TransactionScope。不过我倾向于手动管理Transaction。

代码下载

本次的代码请参考这个changeset

今天就到这里-v-

本文共计1957个文字,预计阅读时间需要8分钟。

如何深入掌握Entity Framework 4.1 Code First的进阶技巧?

写系列篇的上一篇已经是很久以前的事情了==在此期间,EF 4.1的RTW已经发布,NH 3.2的Alpha也出了第二版。其实不是我不懒,工作中一直在使用EF 4.1。主要是上次承诺的一个Update功能还没搞定。

写系列的上一篇已经是很久之前的事儿了= =在此期间,EF 4.1的RTW都已经出来了,NH 3.2的Alpha已经2了。。。其实不是我懒,工作中也在一直使用EF 4.1。主要是上次承诺过的一个Update功能搞不定= =

总之这一次的目标是

  • 实现一个完整的IRepository(添加增删改能力)
  • 领域对象的继承
  • 事物

首先来看IRepository

我的接口如下

1: public interface IRepository<TEntity>

2: where TEntity : IEntity

3: {

4: IEnumerable<TEntity> FindAll();

5: TEntity FindById(int id);

6: void Add(TEntity entity);

7: void Delete(TEntity entity);

8: void Update(TEntity entity);

9: }

应该算是一个最基本的仓储接口了。

其中前几个接口都是很好实现的,上次提及的DbSet对象提供了相应的接口,直接调用即可,代码是类似这样的。

1: protected DbSet<TEntity> DbSet

2: {

3: get { return m_dbContext.Set<TEntity>(); }

4: }

5:

6: public IEnumerable<TEntity> FindAll()

7: {

8: return DbSet;

9: }

10:

11: public TEntity FindById(int id)

12: {

13: return DbSet.SingleOrDefault(entity => entity.Id == id);

14: }

15:

16: public void Add(TEntity entity)

17: {

18: DbSet.Add(entity);

19: m_dbContext.SaveChanges();

20: }

21:

22: public void Delete(TEntity entity)

23: {

24: DbSet.Remove(entity);

25: m_dbContext.SaveChanges();

26: }

关键问题是最后的Update方法

DbSet对象并没有提供相应的接口,为什么呢?因为EF相信自己的Self Tracking能力。也就是说,EF认为把一个entity从context中加载出来,做一些变更,然后直接SaveChanges就可以了,不需要特意提供一个Update方法。

但是这里有一个前提,就是“entity是从context中加载出来”。如果entity是新new出来的呢?比如在MVC里,entity很可能是ModelBinder帮我们new出来的,context对它一无所知,直接SaveChanges显然不会有任何效果。

那么如何让context可以理解一个新new出来的entity呢?这要从EF处理entity状态开始说起。

EF定义了如下几种State(注意这个枚举是Flag)

1: [Flags]

2: public enum EntityState

3: {

4: Detached = 1,

5: Unchanged = 2,

6: Added = 4,

7: Deleted = 8,

8: Modified = 16,

9: }

其中Detached状态,就是entity还没有attach到context(实际上是Attach到某个DbSet上)的状态。具体怎么做呢?直接上代码

1: public void Update(TEntity entity)

2: {

3: var entry = m_dbContext.Entry(entity);

4: if (entry.State == EntityState.Detached)

5: {

6: //DbSet.Attach(entity);

7: entry.State = EntityState.Modified;

8: }

9: m_dbContext.SaveChanges();

10: }

可以看到上面的代码给出了两种办法,一种是直接修改entry的State,另一种是调用DbSet对象的Attach方法。

注意到DbContext.Entry方法取出的DbEntityEntry对象。利用这个对象可以做很多有用的事哦~~园子里的EF专家LingzhiSun有一篇blog,大家可以去读读。

不过这个实现有一个缺陷

我们上面谈到过,上面这个实现实际上是把entity attach到了对应的DbSet上。但是如果你的代码是类似如下的,就可能产生问题(没有亲试,感觉上是这样的= =)

1: var heros = repository.FindAll();

2: var hero = heros.First(h => h.Id == 1);

3: var heroNew = new Hero

4: {

5: Id = hero.Id,

6: Name = hero.Name,

7: Race = hero.Race

8: };

9: repository.Update(heroNew);

应该是会抛出来一个异常说“An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.”

异常说的很明白,你的DbSet已经加载过一次id为1的对象了,当试图去attach另一个id为1的对象的时候EF就会无所适从。

那是不是说刚才给出的那个实现根本就行不通呢?不是的!事实上微软官方的文章上就是采用这种方法的。关键就在于当你尝试去attach一个entity的时候,要保证DbSet还没有加载过!我们看上面那篇微软的文章里是如何保证这一点的

1: public class BlogController : Controller

2: {

3: BlogContext db = new BlogContext();

4:

5: //...

6:

7: [HttpPost]

8: public ActionResult Edit(int id, Blog blog)

9: {

10: try

11: {

12: db.Entry(blog).State = EntityState.Modified;

13: db.SaveChanges();

14:

15: return RedirectToAction("Index");

16: }

17: catch

18: {

19: return View();

20: }

21: }

22: }

很明显,在执行Edit这个Action之前,DbSet没有加载过,因为MVC帮我们保证了DbContext实例是request结束就被销毁的.

也就是说,结论是使用这种Update实现方式对context的生命周期是有要求的.当然我的例子中context的生命周期也是per-request的所以没关系。

那么如果我们想使用其他的context生命周期管理方式呢?比如希望整个application只有一个context实例?

让我们来给出另一种实现

回过头来想一想在实现Update这个方法的时候我们最初遇到的问题:entity不是从context中加载的而是直接new出来的

那么我们手动的来加载一次就好了么,代码类似于这样

1: public void Update(Hero entity)

2: {

3: var entry = m_dbContext.Entry(entity);

4: if (entry.State == EntityState.Detached)

5: {

6: Hero entityToUpdate = FindById(entity.Id);

7: entityToUpdate.Id = entity.Id;

8: entityToUpdate.Name = entity.Name;

9: entityToUpdate.Race = entity.Race;

10: }

11: m_dbContext.SaveChanges();

12: }

不过由于失去了泛型的优势,给每个domain model都要实现一个Update方法比较烦,可以用一些框架来解决这个问题,例如EmitMapper(园子里也讨论过这个东西)

1: public void Update(TEntity entity)

2: {

3: var entry = m_dbContext.Entry(entity);

4: if (entry.State == EntityState.Detached)

5: {

6: var entityToUpdate = FindById(entity.Id);

7: EmitMapper.ObjectMapperManager.DefaultInstance.GetMapper<TEntity, TEntity>().Map(entity, entityToUpdate);

8: }

9: m_dbContext.SaveChanges();

10: }

当然这个实现也有不好的地方例如说当domain里有一些跟ORM没关系的property时也会被EmitMapper改写掉。

下一个议题是领域对象的继承

让领域对象实现继承的好处是不言而喻的,可以使用到多态等OO带来的好处。相对的就对ORM提出了更高的要求。

如何深入掌握Entity Framework 4.1 Code First的进阶技巧?

我们知道映射对象树到数据库有三种经典的实现方式:Table Per Type、Table Per Hierarchy和Table Per Concrete class,这次我们来实践最简单的一种:Table Per Hierarchy。

回想我们上一次的类

1: public class Hero : IEntity

2: {

3: public int Id { get; set; }

4: public string Name { get; set; }

5: public bool IsSuperHero { get; set; }

6: public virtual Race Race { get; set; }

7: }

把它拆成两个有继承关系的类

1: public class Hero : IEntity

2: {

3: public int Id { get; set; }

4: public string Name { get; set; }

5: //public bool IsSuperHero { get; set; }

6: public virtual Race Race { get; set; }

7: }

8: public class SuperHero : Hero

9: {

10:

11: }

在EF Code First中这种单表继承的映射关系是这样来写的

1: Map<Hero>(hero => hero.Requires(ColumnNameMappingStrategy.Value.To("IsSuperHero")).HasValue(false)).ToTable(tableNameMappingStrategy.To("Hero"));

2: Map<SuperHero>(hero => hero.Requires(ColumnNameMappingStrategy.Value.To("IsSuperHero")).HasValue(true)).ToTable(tableNameMappingStrategy.To("Hero"));

另外两种方式的实现也不复杂,可以参考这里。这个实例还是CTP5的API,跟4.1最终版有些区别不过应该影响不大。

今天最后的议题是事物

可以用TransactionScope来管理,虽然看起来有些浪费,毕竟例子中不涉及Transaction传播,连DbContext都只有一个实例。代码如下

1: [HttpPost]

2: public ActionResult Edit(TEntity entity)

3: {

4: try

5: {

6: using (var scope = new TransactionScope())

7: {

8: ModelRepository.Update(entity);

9: scope.Complete();

10: }

11: return RedirectToAction("Index");

12: }

13: catch

14: {

15: return View();

16: }

17: } Spring实际上也可以用AOP的方式管理TransactionScope。不过我倾向于手动管理Transaction。

代码下载

本次的代码请参考这个changeset

今天就到这里-v-