劇情開始
有時候相識即是一種緣分,相愛也不需要太多的理由,一個眼神足矣,當EntityFramework遇上AutoMapper,就是如此,戀愛雖易,相處不易。
在DDD(領域驅動設計)中,使用AutoMapper一般場景是(Domain Layer)領域層與Presentation Layer(表現層)之間數據對象的轉換,也就是DTO與Domin Model之間的相互轉換,但是如果對AutoMapper有深入了解之后,就會發現她所涉及的領域不僅僅局限如此,應該包含所有對象之間的轉換。另一邊,當EntityFramework還在為單身苦惱時,不經意的一瞬間相識了AutoMapper,從此就深深的愛上了她。
AutoMapper是一個強大的Object-Object Mapping工具,關于AutoMapper請參照:
- 【AutoMapper官方文檔】DTO與Domin Model相互轉換(上)
- 【AutoMapper官方文檔】DTO與Domin Model相互轉換(中)
- 【AutoMapper官方文檔】DTO與Domin Model相互轉換(下)
為何相愛?
上面是AutoMapper對象轉換示意圖,可以看出AutoMapper的主要用途是用在對象映射轉換上,她不管是什么對象,只是負責轉換,就像一個女人在家只負責相夫教子一樣。看下AutoMapper的基本用法:
1 // 配置 AutoMapper 2 Mapper.CreateMap<Order, OrderDto>(); 3 // 執行 mapping 4 OrderDto dto = Mapper.Map<Order, OrderDto>(order);
EntityFramework是什么?他是微軟開發的基于ADO.NET的ORM(Object/Relational Mapping)框架,是個大人物,是有身份和地位的人,就像一個“王子”一樣,而AutoMapper準確的來說只是一個小角色,就像“灰姑娘”一樣,況且他們也不是一個世界的人,那為什么EntityFramework會看上AutoMapper呢?這里面必定有內情,我們來探查一番。
假如存在這樣一個業務場景,Order表中存在百萬條訂單數據,而且Order表有幾百列,根據業務場景要求,我們要對訂單進行分離,比如:客戶信息訂單、產品訂單等等,可能只是用到訂單表中的某些字段,如果我們去做這樣的一個操作,可以想象這樣查詢出的數據是怎樣的,某些我們并不需要的字段會查詢出來,而且數據并沒有得到過濾,所以我們要在數據訪問層做下面這樣一個操作:
1 using (var context = new OrderContext()) 2 { 3 var orderConsignee = from order in context.Orders 4 select new OrderConsignee 5 { 6 OrderConsigneeId = order.OrderId, 7 //OrderItems = order.OrderItems, 8 OrderItemCount = order.OrderItemCount, 9 ConsigneeName = order.ConsigneeName, 10 ConsigneeRealName = order.ConsigneeRealName, 11 ConsigneePhone = order.ConsigneePhone, 12 ConsigneeProvince = order.ConsigneeProvince, 13 ConsigneeAddress = order.ConsigneeAddress, 14 ConsigneeZip = order.ConsigneeZip, 15 ConsigneeTel = order.ConsigneeTel, 16 ConsigneeFax = order.ConsigneeFax, 17 ConsigneeEmail = order.ConsigneeEmail 18 };
19 Console.ReadKey(); 20 }
orderConsignee表示訂單客戶,這只是訂單信息分離的一種子集,如果有多種分離的子集,并且子集中的字段并不比訂單表少多少,你就會發現在數據訪問層填充這些子集要做的工作量有多少了,雖然它是高效的,從生成的SQL代碼中就可以看出:
1 SELECT 2 [Extent1].[OrderItemCount] AS [OrderItemCount], 3 [Extent1].[OrderId] AS [OrderId], 4 [Extent1].[ConsigneeName] AS [ConsigneeName], 5 [Extent1].[ConsigneeRealName] AS [ConsigneeRealName], 6 [Extent1].[ConsigneePhone] AS [ConsigneePhone], 7 [Extent1].[ConsigneeProvince] AS [ConsigneeProvince], 8 [Extent1].[ConsigneeAddress] AS [ConsigneeAddress], 9 [Extent1].[ConsigneeZip] AS [ConsigneeZip], 10 [Extent1].[ConsigneeTel] AS [ConsigneeTel], 11 [Extent1].[ConsigneeFax] AS [ConsigneeFax], 12 [Extent1].[ConsigneeEmail] AS [ConsigneeEmail] 13 FROM [dbo].[Orders] AS [Extent1]
但是這種效果并不能讓EntityFramework滿意,于是他就盯上了人家AutoMapper,為什么?因為AutoMapper的一段代碼就可以搞定上面的問題:
1 OrderDto dto = Mapper.Map<Order, OrderDto>(order);
相處的問題?
因為EntityFramework的瘋狂追求,再加上他顯赫的地位,讓AutoMapper不得不接受了他,于是他們就交往了,但好像就是后羿和嫦娥的故事一樣,不是一個世界的人,相處起來總會出現一些問題。雖然AutoMapper在對象轉換方面很強大,而且大部分應用場景是Domain與ViewModel之間的映射轉換,當涉及到數據訪問時,AutoMapper就不是那么有用了。換句話說,AutoMapper工作在內存中的對象轉換,而不是應用在數據訪問中IQueryable的接口,在數據訪問層我們使用EntityFramework把要查詢的對象轉化為SQL命令,如果在數據訪問層使用AutoMapper,那么查詢數據一定會發生在映射轉換之后,而且查詢出的數據一定會比轉換的數據多,從而產生性能問題。
上面的示例我們修改下:
1 Mapper.CreateMap<Order, OrderConsignee>(); 2 var details = Mapper.Map<IEnumerable<Order>, IEnumerable<OrderConsignee>>(context.Orders).ToList();
其實這就是EntityFramework看上AutoMapper的原因,也是EntityFramework想要的效果,看下生成的SQL語句:
1 SELECT 2 [Extent1].[OrderId] AS [OrderId], 3 [Extent1].[OrderItemCount] AS [OrderItemCount], 4 [Extent1].[UserId] AS [UserId], 5 [Extent1].[ReceiverId] AS [ReceiverId], 6 [Extent1].[ShopDate] AS [ShopDate], 7 [Extent1].[OrderDate] AS [OrderDate], 8 [Extent1].[ConsigneeRealName] AS [ConsigneeRealName], 9 [Extent1].[ConsigneeName] AS [ConsigneeName], 10 [Extent1].[ConsigneePhone] AS [ConsigneePhone], 11 [Extent1].[ConsigneeProvince] AS [ConsigneeProvince], 12 [Extent1].[ConsigneeAddress] AS [ConsigneeAddress], 13 [Extent1].[ConsigneeZip] AS [ConsigneeZip], 14 [Extent1].[ConsigneeTel] AS [ConsigneeTel], 15 [Extent1].[ConsigneeFax] AS [ConsigneeFax], 16 [Extent1].[ConsigneeEmail] AS [ConsigneeEmail], 17 [Extent1].[WhetherCouAndinte] AS [WhetherCouAndinte], 18 [Extent1].[ParvalueAndInte] AS [ParvalueAndInte], 19 [Extent1].[PaymentType] AS [PaymentType], 20 [Extent1].[Payment] AS [Payment], 21 [Extent1].[Courier] AS [Courier], 22 [Extent1].[TotalPrice] AS [TotalPrice], 23 [Extent1].[FactPrice] AS [FactPrice], 24 [Extent1].[Invoice] AS [Invoice], 25 [Extent1].[Remark] AS [Remark], 26 [Extent1].[OrderStatus] AS [OrderStatus], 27 [Extent1].[SaleUserID] AS [SaleUserID], 28 [Extent1].[SaleUserType] AS [SaleUserType], 29 [Extent1].[BusinessmanID] AS [BusinessmanID], 30 [Extent1].[Carriage] AS [Carriage], 31 [Extent1].[PaymentStatus] AS [PaymentStatus], 32 [Extent1].[OgisticsStatus] AS [OgisticsStatus], 33 [Extent1].[OrderType] AS [OrderType], 34 [Extent1].[IsOrderNormal] AS [IsOrderNormal] 35 FROM [dbo].[Orders] AS [Extent1]
通過上面的SQL語句,會發現,雖然數據訪問層代碼寫的簡單了,但是查詢的字段并不是我們想要的,也就是說查詢發生在映射之前,可以想象如果存在上百萬的數據或是上百行,使用AutoMapper進行映射轉換是多么的不靠譜,難道EntityFramework和AutoMapper就沒有緣分?或者只是EntityFramework的一廂情愿?請看下面。
女人的偉大?
在EntityFramework和AutoMapper的相處過程中,雖然出現了某些問題,但其實也并不是EntityFramework的錯,錯就錯在他們生不逢地,通過相處AutoMapper也發現EntityFramework是真心對她好,于是AutoMapper決定要做些改變,為了EntityFramework,也為了他們的將來。
EntityFramework和AutoMapper不在一個世界的原因,前面我們也分析過,一個存在于內存中,一個存在于數據訪問中,AutoMapper要做的就是去擴展IQueryable表達式(有點嫦娥下凡的意思哈),從而使他們可以存在于一個世界,于是她為了EntityFramework就做了以下工作:
1 public static class QueryableExtensions 2 { 3 public static ProjectionExpression<TSource> Project<TSource>(this IQueryable<TSource> source) 4 { 5 return new ProjectionExpression<TSource>(source); 6 } 7 } 8 9 public class ProjectionExpression<TSource> 10 { 11 private static readonly Dictionary<string, Expression> ExpressionCache = new Dictionary<string, Expression>(); 12 13 private readonly IQueryable<TSource> _source; 14 15 public ProjectionExpression(IQueryable<TSource> source) 16 { 17 _source = source; 18 } 19 20 public IQueryable<TDest> To<TDest>() 21 { 22 var queryExpression = GetCachedExpression<TDest>() ?? BuildExpression<TDest>(); 23 24 return _source.Select(queryExpression); 25 } 26 27 private static Expression<Func<TSource, TDest>> GetCachedExpression<TDest>() 28 { 29 var key = GetCacheKey<TDest>(); 30 31 return ExpressionCache.ContainsKey(key) ? ExpressionCache[key] as Expression<Func<TSource, TDest>> : null; 32 } 33 34 private static Expression<Func<TSource, TDest>> BuildExpression<TDest>() 35 { 36 var sourceProperties = typeof(TSource).GetProperties(); 37 var destinationProperties = typeof(TDest).GetProperties().Where(dest => dest.CanWrite); 38 var parameterExpression = Expression.Parameter(typeof(TSource), "src"); 39 40 var bindings = destinationProperties 41 .Select(destinationProperty => BuildBinding(parameterExpression, destinationProperty, sourceProperties)) 42 .Where(binding => binding != null); 43 44 var expression = Expression.Lambda<Func<TSource, TDest>>(Expression.MemberInit(Expression.New(typeof(TDest)), bindings), parameterExpression); 45 46 var key = GetCacheKey<TDest>(); 47 48 ExpressionCache.Add(key, expression); 49 50 return expression; 51 } 52 53 private static MemberAssignment BuildBinding(Expression parameterExpression, MemberInfo destinationProperty, IEnumerable<PropertyInfo> sourceProperties) 54 { 55 var sourceProperty = sourceProperties.FirstOrDefault(src => src.Name == destinationProperty.Name); 56 57 if (sourceProperty != null) 58 { 59 return Expression.Bind(destinationProperty, Expression.Property(parameterExpression, sourceProperty)); 60 } 61 62 var propertyNames = SplitCamelCase(destinationProperty.Name); 63 64 if (propertyNames.Length == 2) 65 { 66 sourceProperty = sourceProperties.FirstOrDefault(src => src.Name == propertyNames[0]); 67 68 if (sourceProperty != null) 69 { 70 var sourceChildProperty = sourceProperty.PropertyType.GetProperties().FirstOrDefault(src => src.Name == propertyNames[1]); 71 72 if (sourceChildProperty != null) 73 { 74 return Expression.Bind(destinationProperty, Expression.Property(Expression.Property(parameterExpression, sourceProperty), sourceChildProperty)); 75 } 76 } 77 } 78 79 return null; 80 } 81 82 private static string GetCacheKey<TDest>() 83 { 84 return string.Concat(typeof(TSource).FullName, typeof(TDest).FullName); 85 } 86 87 private static string[] SplitCamelCase(string input) 88 { 89 return Regex.Replace(input, "([A-Z])", " $1", RegexOptions.Compiled).Trim().Split(' '); 90 } 91 }
修改示例代碼:
1 Mapper.CreateMap<Order, OrderConsignee>(); 2 var details = context.Orders.Project().To<OrderConsignee>();
通過AutoMapper所做的努力,使得代碼更加簡化,只要配置一個類型映射,傳遞目標類型,就可以得到我們想要的轉換對象,代碼如此簡潔,我們再來看下生成SQL代碼:
1 SELECT 2 [Project1].[OrderId] AS [OrderId], 3 [Project1].[OrderItemCount] AS [OrderItemCount], 4 [Project1].[ConsigneeRealName] AS [ConsigneeRealName], 5 [Project1].[ConsigneeName] AS [ConsigneeName], 6 [Project1].[ConsigneePhone] AS [ConsigneePhone], 7 [Project1].[ConsigneeProvince] AS [ConsigneeProvince], 8 [Project1].[ConsigneeAddress] AS [ConsigneeAddress], 9 [Project1].[ConsigneeZip] AS [ConsigneeZip], 10 [Project1].[ConsigneeTel] AS [ConsigneeTel], 11 [Project1].[ConsigneeFax] AS [ConsigneeFax], 12 [Project1].[ConsigneeEmail] AS [ConsigneeEmail], 13 [Project1].[C1] AS [C1], 14 [Project1].[OrderItemId] AS [OrderItemId], 15 [Project1].[ProName] AS [ProName], 16 [Project1].[ProImg] AS [ProImg], 17 [Project1].[ProPrice] AS [ProPrice], 18 [Project1].[ProNum] AS [ProNum], 19 [Project1].[AddTime] AS [AddTime], 20 [Project1].[ProOtherPara] AS [ProOtherPara], 21 [Project1].[Order_OrderId] AS [Order_OrderId] 22 FROM ( SELECT 23 [Extent1].[OrderId] AS [OrderId], 24 [Extent1].[OrderItemCount] AS [OrderItemCount], 25 [Extent1].[ConsigneeRealName] AS [ConsigneeRealName], 26 [Extent1].[ConsigneeName] AS [ConsigneeName], 27 [Extent1].[ConsigneePhone] AS [ConsigneePhone], 28 [Extent1].[ConsigneeProvince] AS [ConsigneeProvince], 29 [Extent1].[ConsigneeAddress] AS [ConsigneeAddress], 30 [Extent1].[ConsigneeZip] AS [ConsigneeZip], 31 [Extent1].[ConsigneeTel] AS [ConsigneeTel], 32 [Extent1].[ConsigneeFax] AS [ConsigneeFax], 33 [Extent1].[ConsigneeEmail] AS [ConsigneeEmail], 34 [Extent2].[OrderItemId] AS [OrderItemId], 35 [Extent2].[ProName] AS [ProName], 36 [Extent2].[ProImg] AS [ProImg], 37 [Extent2].[ProPrice] AS [ProPrice], 38 [Extent2].[ProNum] AS [ProNum], 39 [Extent2].[AddTime] AS [AddTime], 40 [Extent2].[ProOtherPara] AS [ProOtherPara], 41 [Extent2].[Order_OrderId] AS [Order_OrderId], 42 CASE WHEN ([Extent2].[OrderItemId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1] 43 FROM [dbo].[Orders] AS [Extent1] 44 LEFT OUTER JOIN [dbo].[OrderItems] AS [Extent2] ON [Extent1].[OrderId] = [Extent2].[Order_OrderId] 45 ) AS [Project1] 46 ORDER BY [Project1].[OrderId] ASC, [Project1].[C1] ASC
可以看出因為Order和OrderConsignee包含對OrderItems子集的映射關系:
1 /// <summary> 2 /// 訂單項 3 /// </summary> 4 public virtual ICollection<OrderItem> OrderItems { get; set; }
所以AutoMapper會自動匹配關聯子集進行查詢,當然也可以在創建映射關系的時候對OrderItems進行忽略:Mapper.CreateMap<Order, OrderConsignee>().ForMember(dest => dest.OrderItems, opt => opt.Ignore()); 排除OrderItems關聯因素,從SQL代碼可以看出并沒有查詢多余的字段,也就是我們想要的效果,這所以的一切都歸功于AutoMapper,也許如果沒有AutoMapper的努力,她和EntityFramework說不準還真不能在一起,女人真是偉大啊。
劇情收尾?
示例代碼下載:http://pan.baidu.com/s/1c0h9TNM
經過一切風風雨雨,EntityFramework終于和AutoMapper過上了幸福美滿的日子,但是看似幸福,但是問題還是不斷,有人又提出疑問:
文章的標題用了“horrible”這個單詞,翻譯為可怕的,難道說EntityFramework和AutoMapper在一起有那么可怕嗎?當然這只是針對EntityFramework使用AutoMapper進行CURD操作,但是我相信EntityFramework和AutoMapper會克服重重困難,生死不渝的。我們也會一直關注他們的婚后生活,未完待續。。。
如果你也祝福EntityFramework和AutoMapper會永遠在一起,那就瘋狂的“戳”右下角的“推薦”吧。^_^
文章列表