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生命周期管理。
  • 事务

  扩展需求

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

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

  场景

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

image  实践(一)

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

image

image

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

  按照官方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&lt;Hero&gt;, 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作为前缀。

  总结

  至此,我们已经能从数据库中把数据取出来了。回顾一下需求,我们大体实现了这样几点:

  1、查
  2、一对多
  3、可以映射到现有数据库上
  4、可以让任意类映射到数据库
  5、pre-request的DbContext生命周期管理。

  下一章预计会讲一个完整的IRepository(添加增删改能力)和领域对象的继承。敬请期待:)

  代码下载

  系列的完整示例在前文提过的CodePlex,本文涉及到的部分请check out这个changeset

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值