Entity Framework 4.1 Code First 学习之路

前言

  公司最近的项目决定使用EF。作为EF的完全新手,写一些学习中的经历和解决的办法,希望老鸟们能不吝赐教。

  sample程序使用EF 4.1RC+Spring.Net 1.3.1+ASP.NET MVC3。在CodePlex开源:

http://efsample.codeplex.com/

  由于使用了其他的开源框架,还是声明一下license为Apache 2.0。实际上只要不违反各个框架的license,本系列代码请随意使用。

  需求

  先谈一谈项目对ORM的需求。

  基本需求

  • 增删改
  • 一对多
  • 多对多
  • 可以映射到现有数据库上(有一些命名方面的问题)
  • 可以让任意类映射到数据库上(项目允许客户二次开发。最简单的情况下,希望用户只写出类和表结构就可以映射了。这是为什么选择了Code First的主要原因)
  • per-request的DbContext生命周期管理。
  • 事务

  扩展需求

  • 一对一
  • 领域类继承
  • 领域类的依赖注入

  这个系列将尝试覆盖以上的大多数问题。

  场景

  假设我们准备做个游戏,有如下的表结构:

  实践(一)

  作为系列的第一章,我们的目标是:从数据库中取出数据来。像这样:

 

 

 

  让我们开始第一步:创建领域模型

  按照官方blog的walk through,很容易的写出如下代码。

  
  
public class Race : IEntity { public int Id { get ; set ; } public string Name { get ; set ; } } public class Hero : IEntity { public int Id { get ; set ; } public string Name { get ; set ; } public bool IsSuperHero { get ; set ; } public virtual Race Race { get ; set ; } }

  注意Hero类中的Race属性被定义成了virtual,这是为了延迟加载。但是不同于NH的是,不写virtual不会报错,而是在使用时报出空引用异常。

这里插一句,在领域模型中必须向ORM妥协是让我非常不爽的地方,从使用NH的时候我就非常不喜欢virtual这一点。NH lead Ayende Rahien 推荐了virtuosity,可以尝试一下。

  下一步,我们要创建自己的DbContext了

  按照官方blog的walk through上的写法,大概会写成:

  
  
public class EfDbContext : DbContext { public DbSet < Hero > Heros { get ; set ; } public DbSet < Race > Races { get ; set ; } }

  但是这样不就不能完成我们“任意类映射到数据库上”的需求了么?难道可续新加了一个类Abc,我们就要加一行代码public DbSet<Abc> Abcs{get;set;}么?

  解决办法是使用DbContext类的Set方法(说实话从方法名实在看不出这个方法是取DbSet的),用法类似如下:

  
  
public IEnumerable < TEntity > FindAll() { return m_dbContext.Set < TEntity > (); }

  接下来就是读写数据了

  但是在这里让我们稍作停顿,思考一下我们的架构。应该让领域层(及更上层)使用基础结构层的DbContext对象么?不。有如下几点原因:

  • 上层应该对基础结构的实现一无所知,而DbContext是EF的对象,暴露了太多EF的细节。
  • DbContext是数据库访问的入口,提供了很多能力。这些能力是否应该向上层开放是很值得商榷的问题。例如DbContext的生命周期由谁管理?如果上层可以直接使用DbContext那么意味着上层有能力new它Dispose它,也即拥有了管理它生命周期的权利。

  基于上述原因,在领域层抽象出接口,让EF层来实现成了我的选择。在这个系列的第一章里,我们只用到了查询,所以接口也只包含了查询功能。

  
  
public interface IRepository < TEntity > where TEntity : IEntity { IEnumerable < TEntity > FindAll(); } public class EntityRepository < TEntity > : IRepository < TEntity > where TEntity : class , IEntity { private readonly DbContext m_dbContext; public EntityRepository(DbContext dbContext) { m_dbContext = dbContext; } public IEnumerable < TEntity > FindAll() { return m_dbContext.Set < TEntity > (); } }

  领域层和基础设施层的双向依赖是一个很古老的问题。可以用依赖注入框剪来解决。

  
  
< object id ="dbContext" type ="EfSample.Model.Ef.EfDbContext ,EfSample.Model.Ef" scope ="request" > ... </ object > < object id ="repositoryBase" abstract ="true" scope ="request" > < constructor-arg index ="0" ref ="dbContext" /> </ object > < object id ="heroRepository" type ="EfSample.Model.Ef.Repositories.EntityRepository<Hero>, EfSample.Model.Ef" parent ="repositoryBase" />

  可以看到了依赖注入框架顺便替我们解决了DbContext的生命周期问题——per-request的管理。

  到这里,上层就可以使用IRepository接口来读取数据了。但是如果你真的去尝试,会发现立即抛异常,为什么呢?因为EF的Code Firs模式有一套默认的convention来做类型到数据库的映射,而我上面给出的数据库命名实在和默认convention差太远了。

  于是让我们来创建自己的Mapping

  让我们来回顾一下我们的Mapping规则:

  • Type->Table
    • 有tbl前缀
    • 每个单词之间用下划线分割
    • 全部使用小写
  • Property->Column
    • 以类名为前缀
    • 每个单词之间用下划线分割
    • 全部使用小写

  方案看似是很简单的:修改EF默认的conventions就好了嘛~~但是EF Code Firt不提供这个能力。。。

EF Team说这个feature有一些易用性的问题,但是在RC前没时间搞所以就不提供了。。。这里有篇博文讲如何打破这种限制,其实就是用反射将自定的convention强行写到Conventions集合里。本文还是不采用这种方法了。

  只能在写自己的DbContext的时候override OnModelCreating方法。具体来讲有两种做法:

  一是在这个方法里写所有mapping信息:

  
  
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity < Hero > () .ToTable( " tbl_hero " ); modelBuilder.Entity < Hero > () .Property(x => x.Id).HasColumnName( " hero_id " ); foreach (var mapping in Mappings) { mapping.RegistTo(modelBuilder.Configurations); } }

  另一种是为每个类创建自己的EntityTypeConfiguration,把它插入到modelBuilder.Configurations里。本例中采用后一种办法。一来后一种方法隔离了各种Type的Mapping;二来后者对依赖注入也有比较好的支持。

  然而modelBuilder.Configurations的Add方法并不是一个很好的API,要求传入的泛型类型无法协变,写不出这样的代码:

  
  
protected override void OnModelCreating(DbModelBuilder modelBuilder) { foreach (var mapping in Mappings) { modelBuilder.Configurations.Add(mapping); } }

  作为妥协方案,只能让各个mapping类把自己注册到modelBuilder.Configurations里。代码是这种感觉:

  
  
public class EfDbContext : DbContext { public IList < IMapping > Mappings { get ; set ; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { foreach (var mapping in Mappings) { mapping.RegistTo(modelBuilder.Configurations); } } } public class HeroMapping : EntityMappingBase < Hero > , IMapping { public HeroMapping(IMappingStrategy < string > tableNameMappingStrategy) : base (tableNameMappingStrategy) { Property(e => e.Id).HasColumnName(ColumnNameMappingStrategy.Value.To( " Id " )); Property(e => e.Name).HasColumnName(ColumnNameMappingStrategy.Value.To( " Name " )); Property(e => e.IsSuperHero).HasColumnName(ColumnNameMappingStrategy.Value.To( " IsSuperHero " )); // Property(e => e.Race).HasColumnName(AddUnderscoresBetweenWordsThenToLowerMappingStrategy.Value.To("Race")); HasRequired(h => h.Race).WithMany().Map( config => config.MapKey(ColumnNameMappingStrategy.Value.To( " RaceId " ))); } #region Implementation of IMapping public void RegistTo(ConfigurationRegistrar configurationRegistrar) { configurationRegistrar.Add( this ); } #endregion }

  这样我们就可以注入EfDbContext的Mappings属性了:

  
  
< object id ="dbContext" type ="EfSample.Model.Ef.EfDbContext ,EfSample.Model.Ef" scope ="request" > < property name ="Mappings" > < list element-type ="EfSample.Model.Ef.Mappings.IMapping ,EfSample.Model.Ef" > < ref object ="heroMapping" /> < ref object ="raceMapping" /> </ list > </ property > </ object >

  另外,我也使用了依赖注入tableNameMappingStrategy的方式让mapping有更多的灵活性。不过ColumnNameMappingStrategy就比较难依赖注入了,因为要依赖运行时的TypeName作为前缀。

总之这一次的目标是:
  • 实现一个完整的IRepository(添加增删改能力)
  • 领域对象的继承
  • 事物

  首先来看IRepository

  我的接口如下:

    
    
public interface IRepository < TEntity > where TEntity : IEntity { IEnumerable < TEntity > FindAll(); TEntity FindById( int id); void Add(TEntity entity); void Delete(TEntity entity); void Update(TEntity entity); }
应该算是一个最基本的仓储接口了。

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

  
  
protected DbSet < TEntity > DbSet { get { return m_dbContext.Set < TEntity > (); } } public IEnumerable < TEntity > FindAll() { return DbSet; } public TEntity FindById( int id) { return DbSet.SingleOrDefault(entity => entity.Id == id); } public void Add(TEntity entity) { DbSet.Add(entity); m_dbContext.SaveChanges(); } public void Delete(TEntity entity) { DbSet.Remove(entity); m_dbContext.SaveChanges(); }

  关键问题是最后的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)

  
  
[Flags] public enum EntityState { Detached = 1 , Unchanged = 2 , Added = 4 , Deleted = 8 , Modified = 16 , }

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

  
  
public void Update(TEntity entity) { var entry = m_dbContext.Entry(entity); if (entry.State == EntityState.Detached) { // DbSet.Attach(entity); entry.State = EntityState.Modified; } m_dbContext.SaveChanges(); }

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

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

  不过这个实现有一个缺陷

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

  
  
var heros = repository.FindAll(); var hero = heros.First(h => h.Id == 1 ); var heroNew = new Hero { Id = hero.Id, Name = hero.Name, Race = hero.Race }; 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还没有加载过!我们看上面那篇微软的文章里是如何保证这一点的:

    
    
public class BlogController : Controller { BlogContext db = new BlogContext(); // ... [HttpPost] public ActionResult Edit( int id, Blog blog) { try { db.Entry(blog).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction( " Index " ); } catch { return View(); } } }
很明显,在执行Edit这个Action之前,DbSet没有加载过,因为MVC帮我们保证了DbContext实例是request结束就被销毁的。

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

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

  让我们来给出另一种实现

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

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

  
  
public void Update(Hero entity) { var entry = m_dbContext.Entry(entity); if (entry.State == EntityState.Detached) { Hero entityToUpdate = FindById(entity.Id); entityToUpdate.Id = entity.Id; entityToUpdate.Name = entity.Name; entityToUpdate.Race = entity.Race; } m_dbContext.SaveChanges(); }

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

    
    
public void Update(TEntity entity) { var entry = m_dbContext.Entry(entity); if (entry.State == EntityState.Detached) { var entityToUpdate = FindById(entity.Id); EmitMapper.ObjectMapperManager.DefaultInstance.GetMapper < TEntity, TEntity > ().Map(entity, entityToUpdate); } m_dbContext.SaveChanges(); }
当然这个实现也有不好的地方例如说当domain里有一些跟ORM没关系的property时也会被EmitMapper改写掉。

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

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

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

  回想我们上一次的类:

  
  
public class Hero : IEntity { public int Id { get ; set ; } public string Name { get ; set ; } public bool IsSuperHero { get ; set ; } public virtual Race Race { get ; set ; } }

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

    
    
public class Hero : IEntity { public int Id { get ; set ; } public string Name { get ; set ; } // public bool IsSuperHero { get; set; } public virtual Race Race { get ; set ; } } public class SuperHero : Hero { }
在EF Code First中这种单表继承的映射关系是这样来写的:
  
  
Map < Hero > (hero => hero.Requires(ColumnNameMappingStrategy.Value.To( " IsSuperHero " )).HasValue( false )).ToTable(tableNameMappingStrategy.To( " Hero " )); Map < SuperHero > (hero => hero.Requires(ColumnNameMappingStrategy.Value.To( " IsSuperHero " )).HasValue( true )).ToTable(tableNameMappingStrategy.To( " Hero " ));

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

  今天最后的议题是事物

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

  
  
[HttpPost] public ActionResult Edit(TEntity entity) { try { using (var scope = new TransactionScope()) { ModelRepository.Update(entity); scope.Complete(); } return RedirectToAction( " Index " ); } catch { return View(); } }

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

代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值