Entity Framework 4.1 Code First 學習之路(二)
寫系列的上一篇已經是很久之前的事兒了= =在此期間,EF 4.1的RTW都已經出來了,NH 3.2的Alpha已經2了。。。其實不是我懶,工作中也在一直使用EF 4.1。主要是上次承諾過的一個Update功能搞不定= =
總之這一次的目標是:
- 實現一個完整的IRepository(添加增刪改能力)
- 領域對象的繼承
- 事物
首先來看IRepository
我的接口如下:
where TEntity : IEntity
{
IEnumerable<TEntity> FindAll();
TEntity FindById(int id);
void Add(TEntity entity);
void Delete(TEntity entity);
void Update(TEntity entity);
}
其中前幾個接口都是很好實現的,上次提及的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)
public enum EntityState
{
Detached = 1,
Unchanged = 2,
Added = 4,
Deleted = 8,
Modified = 16,
}
其中Detached狀態,就是entity還沒有attach到context(實際上是Attach到某個DbSet上)的狀態。具體怎么做呢?直接上代碼:
{
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 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還沒有加載過!我們看上面那篇微軟的文章里是如何保證這一點的:
{
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();
}
}
}
也就是說,結論是使用這種Update實現方式對context的生命周期是有要求的.當然我的例子中context的生命周期也是per-request的所以沒關系。
那么如果我們想使用其他的context生命周期管理方式呢?比如希望整個application只有一個context實例?
讓我們來給出另一種實現
回過頭來想一想在實現Update這個方法的時候我們最初遇到的問題:entity不是從context中加載的而是直接new出來的。
那么我們手動的來加載一次就好了么,代碼類似于這樣:
{
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(園子里也討論過這個東西)
{
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();
}
下一個議題是領域對象的繼承
讓領域對象實現繼承的好處是不言而喻的,可以使用到多態等OO帶來的好處。相對的就對ORM提出了更高的要求。
我們知道映射對象樹到數據庫有三種經典的實現方式:Table Per Type、Table Per Hierarchy和Table Per Concrete class,這次我們來實踐最簡單的一種:Table Per Hierarchy。
回想我們上一次的類:
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsSuperHero { get; set; }
public virtual Race Race { get; set; }
}
把它拆成兩個有繼承關系的類:
{
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
{
}
Map<SuperHero>(hero => hero.Requires(ColumnNameMappingStrategy.Value.To("IsSuperHero")).HasValue(true)).ToTable(tableNameMappingStrategy.To("Hero"));
另外兩種方式的實現也不復雜,可以參考這里。這個實例還是CTP5的API,跟4.1最終版有些區別不過應該影響不大。
今天最后的議題是事物
可以用TransactionScope來管理,雖然看起來有些浪費,畢竟例子中不涉及Transaction傳播,連DbContext都只有一個實例。代碼如下:
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-
留言列表