Entity Framework 4.1 Code First 學習之路(一)

作者: jiaxingseng  來源: 博客園  發布時間: 2011-04-24 21:51  閱讀: 37771 次  推薦: 18   原文鏈接   [收藏]  

  前言

  公司最近的項目決定使用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

18
0
 
 
 

文章列表

arrow
arrow
    全站熱搜

    大師兄 發表在 痞客邦 留言(0) 人氣()