文章出處

劇情開始

  有時候相識即是一種緣分,相愛也不需要太多的理由,一個眼神足矣,當EntityFramework遇上AutoMapper,就是如此,戀愛雖易,相處不易。

  在DDD(領域驅動設計)中,使用AutoMapper一般場景是(Domain Layer)領域層與Presentation Layer(表現層)之間數據對象的轉換,也就是DTO與Domin Model之間的相互轉換,但是如果對AutoMapper有深入了解之后,就會發現她所涉及的領域不僅僅局限如此,應該包含所有對象之間的轉換。另一邊,當EntityFramework還在為單身苦惱時,不經意的一瞬間相識了AutoMapper,從此就深深的愛上了她。

  AutoMapper是一個強大的Object-Object Mapping工具,關于AutoMapper請參照:

為何相愛?

  上面是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會永遠在一起,那就瘋狂的“戳”右下角的“推薦”吧。^_^


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


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

    IT工程師數位筆記本

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