寫在前面
上一篇《戀愛雖易,相處不易:當EntityFramework愛上AutoMapper》文章的最后提到,雖然AutoMapper為了EntityFramework做了一些改變,然后就看似幸福的在一起了,但是有人(Roger Alsing)看不下去,寫了一篇文章:http://rogeralsing.com/2013/12/01/why-mapping-dtos-to-entities-using-automapper-and-entityframework-is-horrible/,文中提到EntityFramework和AutoMapper在一起是可怕的,關於這篇英文文章,作為3.5級都考兩次的我來說真是天書,無奈一邊翻譯一邊猜測看了幾遍,英文好的同學可以提供下中文版,因為文章中有代碼,所以作者表達的意思還是可以理解一些。
文章標題主要關鍵字:mapping DTOs to Entities,注意並不是“Entities to DTOs”,表示實體對象到DTO的轉換,並做一些CURD操作,代碼看似合理,但是好像又有些不合理,為什么?主要是對所涉及的概念不清晰,EntityFramework、AutoMapper和DTO,他們到底是什么?有沒有在一起的可能?請接着往下看。
我到底是什么?
EntityFramework(父親)、AutoMapper(母親)和DTO(孩子)你我都知道的官方定義:
- EntityFramework:是微軟以 ADO.NET 為基礎所發展出來的對象關系對應 (O/R Mapping) 解決方案。
- AutoMapper:Object-Object Mapping工具。
- DTO:數據傳輸對象(Data Transfer Object)。
EntityFramework的定義中有“ORM“關鍵字,那ORM又是什么?
ORM:對象關系映射(Object/Relation Mapping),是隨着面向對象的軟件開發方法發展而產生的。面向對象的開發方法是當今企業級應用開發環境中的主流開發方法,關系數據庫是企業級應用環境中永久存放數據的主流數據存儲系統。對象和關系數據是業務實體的兩種表現形式,業務實體在內存中表現為對象,在數據庫中表現為關系數據。內存中的對象之間存在關聯和繼承關系,而在數據庫中,關系數據無法直接表達多對多關聯和繼承關系。 --百度百科
概念清楚了就好辦了,我們再來分析下,從上面定義可以看出:AutoMapper是Object-Object映射工具,EntityFramework是一種ORM框架,DTO是數據傳輸對象(Data Transfer Object),請注意這三個定義中都包含對象(Object)關鍵字,毫無疑問,AutoMapper所做的工作就是ORM中的“O”和DTO中的“O”之間的映射轉換。
DTO中的“O”比較好理解,就是數據傳輸對象,不包含任何的業務邏輯,只是存儲數據的一種對象,用於各層之間的數據傳遞。一般的項目都會采用分層設計(也就是常見的三層架構),每一層都是一個相對內聚的設計,一種松耦合結構。而層與層之間進行通訊的就是DTO,而這個“O”常常不是ORM的O。其實也可能不是DomainEntity,也不是ViewModel,但是它卻有可能通過組合、分解等方式進行轉換。
那ORM中“O”是什么意思?關於ORM的使用,網上有很多的爭論,拋開性能問題,有人提出說“ORM注定了業務邏輯和數據庫的高度耦合”,我覺得這種理解是錯誤的。ORM的“O”是數據對象,與表等有一定的偶合,但它從架構設計上來說,只是倉儲層的內聚設計,與業務邏輯無關(當然現在很多小系統會用它來直接替代業務邏輯對象),而真正的業務邏輯對象(按領域驅動設計來說)是領域對象,真正的的系統核心對象是領域對象,而數據對象是可變的,領域對象則相對穩定,數據對象到領域對象是通過倉儲層的適配器來實現的。
從上面的結論中可以看出,ORM中的“O”是數據對象。關於數據對象,有人說數據對象是穩定的,那要看數據是基於“數據”的設計,還是基於“對象”的設計。如果是基於“對象”的設計,那么設計之初,就必須把業務對象分析清楚、我們常把它說成領域對象,其實這個對象基本是穩定的(至少核心部分是穩定,如果核心對象變了,那是另一個話題,需求變了),而數據對象可能就不一定了,有可能SqlServer數據有的類型其它數據庫沒有,同樣隨着重構的進行,為了提高性能,可能會對一些表進行拆分和組合,原先在一個表中的數據,分成了兩個表,或在視圖中了。但不管數據表怎么變化,最終也只是倉儲層內部的實現方式變了,當然ORM的“O”也會變,但出了這一層,一切都還是原來的樣子。
理解這些概念很重要,理解了你會發現,我為什么把EntityFramework看做“父親”,AutoMapper看做“母親”,DTO看做“孩子”,當然這只是某種意義上的關系比作,只有在這三者結合才會出現,比如DTO可以脫離EntityFramework和AutoMapper獨立存在,但他就不是“孩子”的概念了。
越界的可怕
什么叫越界?就是不是你干的事你卻干了,所做的工作超出自己的范圍之外,Roger Alsing的“Horrible”文章我覺得就是在表達這個意思。AutoMapper的工作范圍只是對象之間的映射轉換,也就是說EntityFramework中的“數據對象”到“DTO”之間的映射轉換,但如果涉及到一些數據訪問或是操作,這就是AutoMapper的越界行為,因為這些操作並不在她的職責范圍之內,而應該是EntityFramework所做的工作。
某種意義上,可以看做:AutoMapper(母親)只是EntityFramework(父親)和DTO(孩子)之間的橋梁或是溝通,至於賺錢養家的事就交給EntityFramework(父親)去做,如果AutoMapper(母親)幫助EntityFramework(父親)去賺錢養家,可能會造成相反的效果,也就是說AutoMapper(母親)請做好“全職太太”即可。
我們來看下AutoMapper(母親)的“越界行為”:
1 public void CreateOrUpdateOrder(OrderDTO orderDTO) 2 { 3 var config = new ... 4 5 //create an instanced mapper instead of the static API 6 var mapper = new AutoMapper.MapperEngine(config); 7 var context = ... //get some DbContext 8 9 config.CreateMap<OrderDTO,Order>() 10 .ConstructUsing((OrderDTO orderDTO) => 11 { 12 if (orderDTO.Id == 0) 13 { 14 var order = new Order(); 15 context.OrderSet.Add(order); 16 return order; 17 } 18 return context.OrderSet.Include("Details").First(o => o.Id == orderDTO.Id); 19 }) 20 //find out what details no longer exist in the DTO and delete the corresponding entities 21 //from the dbcontext 22 .BeforeMap((dto, o) => 23 { 24 o 25 .Details 26 .Where(d => !dto.Details.Any(ddto => ddto.Id == d.Id)).ToList() 27 .ForEach(deleted => context.DetailSet.Remove(deleted)); 28 }); 29 30 config.CreateMap<DetailDTO, Detail>() 31 .ConstructUsing((DetailDTO detailDTO) => 32 { 33 if (detailDTO.Id == 0) 34 { 35 var detail = new Detail(); 36 context.DetailSet.Add(detail); 37 return detail; 38 } 39 return context.DetailSet.First(d => d.Id == detailDTO.Id); 40 }); 41 42 mapper.Map<OrderDTO,Order>(orderDTO); 43 44 context.SaveChanges(); 45 }
AutoMapper的ConstructUsing的用法,我們在:http://www.cnblogs.com/xishuai/p/3704435.html#xishuai_h1中有講解,ConstructUsing表示自定義類型轉換器,發生在映射之前,對映射的操作做一些處理並返回相應的目標類型,注意這里的處理並不是EntityFramework中的持久化,如果在AutoMapper的自定義類型轉換器中做這些操作,就顯得有點不倫不類了。
關於AutoMapper這樣的“越界行為”,Roger Alsing總結出了其中的優缺點,本人就不翻譯了,以免起到誤讀的效果。
優點:
- Looks simple on paper
- Easy to implement on read side and client side
缺點:
- Bloody horrible to implement on the write side, and gets even worse the larger the DTO is
- Relies on magic names if using AutoMapper
- Network ineffective if dealing with large DTOs
- You lose semantics and intentions, there is no way to know WHY a change happened
做好自己
如果我們在上面代碼中去掉AutoMapper,將會變得如何?請看下面:
1 public void CreateOrUpdateOrder(OrderDTO orderDTO) 2 { 3 var ctx = ... //get some DbContext 4 var order = ctx.OrderSet.FirstOrDefault(o => o.Id == orderDTO.Id); 5 if (order == null) 6 { 7 order = new Order(); 8 ctx.OrderSet.Add(order); 9 } 10 11 //Map properties 12 order.Address = orderDTO.Address; 13 14 //remove deleted details 15 order.Details 16 .Where(d => !orderDTO.Details.Any(detailDTO => detailDTO.Id == d.Id)) 17 .Each(deleted => ctx.DetailSet.Remove(deleted)); 18 19 //update or add details 20 orderDTO.Details.Each(detailDTO => 21 { 22 var detail = order.Details.FirstOrDefault(d => d.Id == detailDTO.Id); 23 if (detail == null) 24 { 25 detail = new Detail(); 26 order.Details.Add(detail); 27 } 28 detail.Name = detailDTO.Name; 29 detail.Quantity = detailDTO.Quantity; 30 }); 31 32 context.SaveChanges(); 33 }
這樣代碼更加清潔,與使用AutoMapper的代碼形成了明顯的對比,但如果去掉AutoMapper也就失去了DTO的意義,試想沒有母親哪來的孩子?但是如果EntityFramework(父親)、AutoMapper(母親)和DTO(孩子)這三口之家想和諧的生活在一起,那怎么辦?就是AutoMapper只要負責對象映射轉換即可,也就是做EntityFramework(父親)和DTO(孩子)之間的“溝通橋梁”,也就是“全職太太”:
1 Mapper.CreateMap<OrderDTO, Order>(); 2 Mapper.CreateMap<DetailDTO, Detail>(); 3 using (var context = new OrderContext()) 4 { 5 var existOrder = context.Orders.FirstOrDefault(order => order.Id == orderDTO.Id); 6 if (existOrder == null) 7 { 8 var order = Mapper.Map<OrderDTO, Order>(orderDTO); 9 context.Orders.Add(order); 10 context.Details.AddRange(order.Details); 11 context.SaveChanges(); 12 } 13 }
后記
示例代碼下載:http://pan.baidu.com/s/1o6EzFq6
做了“全職太太”的AutoMapper,就這樣和EntityFramework幸福的生活下去了,看到這有人又可能有疑問,上篇中AutoMapper為EntityFramework做的IQueryable擴展是不是“越界行為”,注意QueryableExtensions只是AutoMapper所做的擴展,並不是代替EntityFramework去完成持久化操作。
如果你覺得EntityFramework和AutoMapper可以幸福終老,那就瘋狂的“戳”右下角的“推薦”吧。^_^
AutoMapper參考文檔: