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

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

  寫系列的上一篇已經是很久之前的事兒了= =在此期間,EF 4.1的RTW都已經出來了,NH 3.2的Alpha已經2了。。。其實不是我懶,工作中也在一直使用EF 4.1。主要是上次承諾過的一個Update功能搞不定= =

  總之這一次的目標是:

  • 實現一個完整的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。

  代碼下載

  本次的代碼請參考這個changeset

  今天就到這里-v-

6
3
 
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

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