一、前言
二、查询问题分析
(一) 数据查询应该在哪做
(三) 导航属性的查询陷阱
三、查询应该怎么设计
(一) 查询数据集设计
(二) 查询陷阱的应对方案
(三) 匿名对象方案与实体对象方案的对比
四、需求实现
(一) 按属性名称排序
(二) 分页查询
(三) 查询实战
五、源码获取
系列导航
首先对大家表示抱歉,这个系列已经将近一个月没有更新了,相信大家等本篇更新都等得快失望了。实在没办法,由于本人水平有限,写篇博客基本上要大半天的时间,最近实在是抽不出这么长段的空闲时间来写。另外也是一直没想好本篇应该怎样写比较容易理解,于是就一天一天的拖着了。废话不多说,言归正传。
EF的CodeFirst是个好东西,让我们完全不用考虑数据库端(注意,这里并不是说不需要对数据库知识进行了解),一切工作都可以通过代码来完成。EF是ORM,已经把数据访问操作封装得很好了,可以直接在业务层中使用,那我们为什么还要对其进行那么多封装呢?在我看来,封装至少能带来如下的好处:
这里就引入一个问题,应该怎样来进行EF的封装呢,既要保证使用的统一与方便性,又要保持EF的灵便性,否则,封装将变成给业务层设置障碍。下面,主要针对数据查询进对可能出现的误用情况进行分析。
在EF中,面向对象的数据查询主要提供了两种方式:
以上两种方式为EF的数据查询提供了极大的自由度,这个自由度是我们在封装的时候需要保持的。但是,在阅读不少人(其中不乏工作了几年的)对EF的封装,设计统一的数据操作接口Repository中关于数据查询的操作中,通常会犯如下几种失误:
诸如此类,各种奇葩的查询操作层出不穷,这些操作或者破坏了EF数据查询原有的灵活性,或者画蛇添足。
其实,这么多失误的原因只有一个,设计者忘记了EF是ORM,把EF当作ado.net来使用了。只要记着EF是ORM,以上这些功能已经实现了,就不要去重复实现了。那么以上的问题就非常好解决了,只要:
在数据操作Repository接口中把EF的DbSet<TEntity>开放成一个只读的IQueryable<TEntity>类型的属性提供给业务层作为数据查询的数据源
就可以了。这个数据源是只读的,并且类型是IQueryable<T>,就保证了它只能作为数据查询的数据源,而不像开放了DbSet<T>类型那样可以在业务层中调用EF的内部方法进行增、删、改等操作。另外IQueryable<T>类型保持了EF原有的查询自由性与灵活性,简单明了。这个数据集还可以传递到业务层的各个层次,以实现在哪需要数据就在哪查的灵活性。
EF的导航属性是延迟加载的,延迟加载的优点就是不用到不加载,一次只加载必要的数据,这减少了每次加载的数据量,但缺点也不言自明:极大的增加了数据库连接的次数,比如如下这么个简单的需求:
输出每个用户拥有的角色数量
根据这个需求,很容易就写出了如下的代码:
遍历所有用户信息,输出每个用户信息中角色(导航属性)的数量。
上面这段代码逻辑很清晰,看似没有什么问题。我们来分析一下代码的执行过程:
1 SELECT 2 [Extent1].[Id] AS [Id], 3 [Extent1].[UserName] AS [UserName], 4 [Extent1].[Password] AS [Password], 5 [Extent1].[NickName] AS [NickName], 6 [Extent1].[Email] AS [Email], 7 [Extent1].[IsDeleted] AS [IsDeleted], 8 [Extent1].[AddDate] AS [AddDate], 9 [Extent1].[Timestamp] AS [Timestamp], 10 [Extent2].[Id] AS [Id1] 11 FROM [dbo].[Members] AS [Extent1] 12 LEFT OUTER JOIN [dbo].[MemberExtends] AS [Extent2] ON [Extent1].[Id] = [Extent2].[Member_Id]
虽然EF生成的SQL有些复杂,但还是没什么问题
1 exec sp_executesql N'SELECT 2 [Extent2].[Id] AS [Id], 3 [Extent2].[Name] AS [Name], 4 [Extent2].[Description] AS [Description], 5 [Extent2].[RoleTypeNum] AS [RoleTypeNum], 6 [Extent2].[IsDeleted] AS [IsDeleted], 7 [Extent2].[AddDate] AS [AddDate], 8 [Extent2].[Timestamp] AS [Timestamp] 9 FROM [dbo].[RoleMembers] AS [Extent1] 10 INNER JOIN [dbo].[Roles] AS [Extent2] ON [Extent1].[Role_Id] = [Extent2].[Id] 11 WHERE [Extent1].[Member_Id] = @EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=1
试想,如果有100个用户,就要连接100次数据库,这么一个简单的需求,连接了101次数据库,还不得让数据库疯掉了。
当然,有同学可以要说,这里用了延迟加载才会多了很多连接数据库的次数,你可以立即加载啊,把Role角色一次性加载进来。好吧,我们来看看立即加载:
143行,在取所有用户信息的时候使用Include方法把与用户关联的所有角色信息也一并查询出来了,这样在循环遍历的时候就不会再连接数据库去查询角色信息了。但是如果看到执行的SQL语句,估计你想死的心情都有了。执行的查询如下:
1 SELECT 2 [Project1].[Id] AS [Id], 3 [Project1].[UserName] AS [UserName], 4 [Project1].[Password] AS [Password], 5 [Project1].[NickName] AS [NickName], 6 [Project1].[Email] AS [Email], 7 [Project1].[IsDeleted] AS [IsDeleted], 8 [Project1].[AddDate] AS [AddDate], 9 [Project1].[Timestamp] AS [Timestamp], 10 [Project1].[Id1] AS [Id1], 11 [Project1].[C1] AS [C1], 12 [Project1].[Id2] AS [Id2], 13 [Project1].[Name] AS [Name], 14 [Project1].[Description] AS [Description], 15 [Project1].[RoleTypeNum] AS [RoleTypeNum], 16 [Project1].[IsDeleted1] AS [IsDeleted1], 17 [Project1].[AddDate1] AS [AddDate1], 18 [Project1].[Timestamp1] AS [Timestamp1] 19 FROM ( SELECT 20 [Extent1].[Id] AS [Id], 21 [Extent1].[UserName] AS [UserName], 22 [Extent1].[Password] AS [Password], 23 [Extent1].[NickName] AS [NickName], 24 [Extent1].[Email] AS [Email], 25 [Extent1].[IsDeleted] AS [IsDeleted], 26 [Extent1].[AddDate] AS [AddDate], 27 [Extent1].[Timestamp] AS [Timestamp], 28 [Extent2].[Id] AS [Id1], 29 [Join2].[Id] AS [Id2], 30 [Join2].[Name] AS [Name], 31 [Join2].[Description] AS [Description], 32 [Join2].[RoleTypeNum] AS [RoleTypeNum], 33 [Join2].[IsDeleted] AS [IsDeleted1], 34 [Join2].[AddDate] AS [AddDate1], 35 [Join2].[Timestamp] AS [Timestamp1], 36 CASE WHEN ([Join2].[Member_Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1] 37 FROM [dbo].[Members] AS [Extent1] 38 LEFT OUTER JOIN [dbo].[MemberExtends] AS [Extent2] ON [Extent1].[Id] = [Extent2].[Member_Id] 39 LEFT OUTER JOIN (SELECT [Extent3].[Member_Id] AS [Member_Id], [Extent4].[Id] AS [Id], [Extent4].[Name] AS [Name], [Extent4].[Description] AS [Description], [Extent4].[RoleTypeNum] AS [RoleTypeNum], [Extent4].[IsDeleted] AS [IsDeleted], [Extent4].[AddDate] AS [AddDate], [Extent4].[Timestamp] AS [Timestamp] 40 FROM [dbo].[RoleMembers] AS [Extent3] 41 INNER JOIN [dbo].[Roles] AS [Extent4] ON [Extent4].[Id] = [Extent3].[Role_Id] ) AS [Join2] ON [Extent1].[Id] = [Join2].[Member_Id] 42 ) AS [Project1] 43 ORDER BY [Project1].[Id] ASC, [Project1].[Id1] ASC, [Project1].[C1] ASC
我们再来回顾一下导航属性的长相(以用户信息中的角色信息为例):
可以看到,集合类的导航属性是一个ICollection<T>类型的集合,其实现类可以是通常使用List<T>或者HashSet<T>。用了ICollection<T>,就限定了集合类的导航属性是一个内存集合,只要用到这个导航属性,就必须把集合中的所有数据都加载到内存中,才能进行后续操作。比如上面的例子中,我们的需求只是想知道用户拥有角色的数量,原意只是要执行一下SQL的Count语句即可,却想不到EF是把这个集合加载到内存中(上面的语句,是把当前用户的所有角色信息查询出来),再在内存中进行计数,这无形中是一个很大的资源浪费。比如在一个商城系统中,我们想了解一种商品的销量(product.Orders.Count),那就可能把几万条订单信息都加载到内存中,再进行计数,这将是灾难性的资源消耗。
读到这里,是不是对EF非常失望?
上面的问题,在项目的开发阶段,根本不是问题,因为软件照样能跑得起来,而且跑得好好的。但是等网站上线的时候,用户量上来的时候,这些性能杀手就暴露无遗了。是问题,总要想办法解决的。
下面就来说说我的解决方案,至于方案靠谱不靠谱,读者自行判断。
在前面的设计中,实体的数据仓储接口已经向上层暴露了一个IQueryable<TEntity>的接口了,为什么暴露这个接口,上面也说了很多了。下面,以账户模块为例,我们就来看看怎样把这个查询数据集往上传递。
首先,不要忘了,我们的项目结构是这样的:
对于前面提到的EF的查询陷阱,我提出的解决方案就是
通过IQueryable<T>的 Select(selector) 扩展方法来按需查询。
首先分析好当前业务中需要什么数据,要什么取什么,最后的数据用匿名对象装载。
比如前面提到的 输出用户拥有的角色数量 这个需求,实现方案如下:
以上代码执行的查询语句如下:
1 SELECT 2 [Extent1].[Id] AS [Id], 3 (SELECT 4 COUNT(1) AS [A1] 5 FROM [dbo].[RoleMembers] AS [Extent2] 6 WHERE [Extent1].[Id] = [Extent2].[Member_Id]) AS [C1] 7 FROM [dbo].[Members] AS [Extent1]
相当简洁,这都是我们需要的效果。
匿名对象的方案虽然达到了我们想要的效果,但对比实体对象方案,又有什么不同呢,下面我们来对比一下:
通过上面的对比,希望能对方案的选择提供一些参考,至于如何取舍,最终选择什么方案,只能自己根据业务的特点来权衡了,合适用哪个就用哪个。
前面已经说过不少次了,这里在明确的提一次,在这个架构设计中,如果现有查询方法不能满足业务需求,需要添加一个相应的查询功能,你不需要到数据层去进行操作,你只需要:
扩展IQueryable<T>,给IQueryable<T>添加一个扩展方法。
查询离不开分页查询,分页查询之前通常会先排序,再查出指定页的单页数据,先来说说按属性排序的问题吧。
排序可以使用IQueryable<T>的OrderBy、OrderByDescending两个扩展方法来进行,例如:
1 source.OrderBy(m => m.AddDate).ThenByDescending(m => m.IsDeleted);
这是系统提供的排序方法,但只支持 Expression<Func<TSource, TKey>> keySelector 类型的参数,而我们在点击表格的表头的时候,通常获取到的是实体的属性名称的字符串,所以我们还需要扩展一个支持属性名称的排序方法。
首先,定义一个类来封装排序条件,排序条件通常包括属性名称与排序方向:
1 namespace GMF.Component.Tools 2 { 3 /// <summary> 4 /// 属性排序条件信息类 5 /// </summary> 6 public class PropertySortCondition 7 { 8 /// <summary> 9 /// 构造一个指定属性名称的升序排序的排序条件 10 /// </summary> 11 /// <param name="propertyName">排序属性名称</param> 12 public PropertySortCondition(string propertyName) 13 : this(propertyName, ListSortDirection.Ascending) { } 14 15 /// <summary> 16 /// 构造一个排序属性名称和排序方式的排序条件 17 /// </summary> 18 /// <param name="propertyName">排序属性名称</param> 19 /// <param name="listSortDirection">排序方式</param> 20 public PropertySortCondition(string propertyName, ListSortDirection listSortDirection) 21 { 22 PropertyName = propertyName; 23 ListSortDirection = listSortDirection; 24 } 25 26 /// <summary> 27 /// 获取或设置 排序属性名称 28 /// </summary> 29 public string PropertyName { get; set; } 30 31 /// <summary> 32 /// 获取或设置 排序方向 33 /// </summary> 34 public ListSortDirection ListSortDirection { get; set; } 35 } 36 }
其次,我们接收的是排序条件是属性名称的字符串,实际还是要调用系统提供的Expression<Func<TSource, TKey>> keySelector类型参数的排序方法进行排序。所以我们还需要一个把字符串条件转换为排序表达式,并调用系统的排序方法。
1 private static class QueryableHelper<T> 2 { 3 // ReSharper disable StaticFieldInGenericType 4 private static readonly ConcurrentDictionary<string, LambdaExpression> Cache = new ConcurrentDictionary<string, LambdaExpression>(); 5 6 internal static IOrderedQueryable<T> OrderBy(IQueryable<T> source, string propertyName, ListSortDirection sortDirection) 7 { 8 dynamic keySelector = GetLambdaExpression(propertyName); 9 return sortDirection == ListSortDirection.Ascending 10 ? Queryable.OrderBy(source, keySelector) 11 : Queryable.OrderByDescending(source, keySelector); 12 } 13 14 internal static IOrderedQueryable<T> ThenBy(IOrderedQueryable<T> source, string propertyName, ListSortDirection sortDirection) 15 { 16 dynamic keySelector = GetLambdaExpression(propertyName); 17 return sortDirection == ListSortDirection.Ascending 18 ? Queryable.ThenBy(source, keySelector) 19 : Queryable.ThenByDescending(source, keySelector); 20 } 21 22 private static LambdaExpression GetLambdaExpression(string propertyName) 23 { 24 if (Cache.ContainsKey(propertyName)) 25 { 26 return Cache[propertyName]; 27 } 28 ParameterExpression param = Expression.Parameter(typeof (T)); 29 MemberExpression body = Expression.Property(param, propertyName); 30 LambdaExpression keySelector = Expression.Lambda(body, param); 31 Cache[propertyName] = keySelector; 32 return keySelector; 33 } 34 }
到此,有了前面的准备,属性名称的排序就非常好写了。为了使用方便,应该做成IQueryable<T>的扩展方法:
1 /// <summary> 2 /// 把IQueryable[T]集合按指定属性与排序方式进行排序 3 /// </summary> 4 /// <param name="source">要排序的数据集</param> 5 /// <param name="propertyName">排序属性名</param> 6 /// <param name="sortDirection">排序方向</param> 7 /// <typeparam name="T">动态类型</typeparam> 8 /// <returns>排序后的数据集</returns> 9 public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string propertyName, 10 ListSortDirection sortDirection = ListSortDirection.Ascending) 11 { 12 PublicHelper.CheckArgument(propertyName, "propertyName"); 13 return QueryableHelper<T>.OrderBy(source, propertyName, sortDirection); 14 } 15 16 /// <summary> 17 /// 把IQueryable[T]集合按指定属性排序条件进行排序 18 /// </summary> 19 /// <typeparam name="T">动态类型</typeparam> 20 /// <param name="source">要排序的数据集</param> 21 /// <param name="sortCondition">列表属性排序条件</param> 22 /// <returns></returns> 23 public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, PropertySortCondition sortCondition) 24 { 25 PublicHelper.CheckArgument(sortCondition, "sortCondition"); 26 return source.OrderBy(sortCondition.PropertyName, sortCondition.ListSortDirection); 27 } 28 29 /// <summary> 30 /// 把IOrderedQueryable[T]集合继续按指定属性排序方式进行排序 31 /// </summary> 32 /// <typeparam name="T">动态类型</typeparam> 33 /// <param name="source">要排序的数据集</param> 34 /// <param name="propertyName">排序属性名</param> 35 /// <param name="sortDirection">排序方向</param> 36 /// <returns></returns> 37 public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, string propertyName, 38 ListSortDirection sortDirection = ListSortDirection.Ascending) 39 { 40 PublicHelper.CheckArgument(propertyName, "propertyName"); 41 return QueryableHelper<T>.ThenBy(source, propertyName, sortDirection); 42 } 43 44 /// <summary> 45 /// 把IOrderedQueryable[T]集合继续指定属性排序方式进行排序 46 /// </summary> 47 /// <typeparam name="T">动态类型</typeparam> 48 /// <param name="source">要排序的数据集</param> 49 /// <param name="sortCondition">列表属性排序条件</param> 50 /// <returns></returns> 51 public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, PropertySortCondition sortCondition) 52 { 53 PublicHelper.CheckArgument(sortCondition, "sortCondition"); 54 return source.ThenBy(sortCondition.PropertyName, sortCondition.ListSortDirection); 55 }
这里使用了ListSortDirection来表示排序方向,当然,你也可以定义ThenByDescending扩展方法来进行反序排序。上面的排序可以写成如下所示:
1 source.OrderBy("AddDate").ThenBy("IsDeleted", ListSortDirection.Descending);
下面来说说分页查询,通常分页查询的设计方法是在仓储操作Repository中定义特定的方法来获取分页的数据,现在我们面对的是IQueryable<T>数据集,就不用那么麻烦了。只要定义一个专用于分页查询的扩展方法即可。代码如下:
1 /// <summary> 2 /// 把IOrderedQueryable[T]集合继续指定属性排序方式进行排序 3 /// </summary> 4 /// <typeparam name="T">动态类型</typeparam> 5 /// <param name="source">要排序的数据集</param> 6 /// <param name="sortCondition">列表属性排序条件</param> 7 /// <returns></returns> 8 public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, PropertySortCondition sortCondition) 9 { 10 PublicHelper.CheckArgument(sortCondition, "sortCondition"); 11 return source.ThenBy(sortCondition.PropertyName, sortCondition.ListSortDirection); 12 } 13 14 /// <summary> 15 /// 从指定 IQueryable[T]集合 中查询指定分页条件的子数据集 16 /// </summary> 17 /// <typeparam name="T">动态类型</typeparam> 18 /// <param name="source">要查询的数据集</param> 19 /// <param name="predicate">查询条件谓语表达式</param> 20 /// <param name="pageIndex">分页索引</param> 21 /// <param name="pageSize">分页大小</param> 22 /// <param name="total">输出符合条件的总记录数</param> 23 /// <param name="sortConditions">排序条件集合</param> 24 /// <returns></returns> 25 public static IQueryable<T> Where<T>(this IQueryable<T> source, Expression<Func<T, bool>> predicate, int pageIndex, int pageSize, 26 out int total, PropertySortCondition[] sortConditions = null) where T : Entity 27 { 28 PublicHelper.CheckArgument(source, "source"); 29 PublicHelper.CheckArgument(predicate, "predicate"); 30 PublicHelper.CheckArgument(pageIndex, "pageIndex"); 31 PublicHelper.CheckArgument(pageSize, "pageSize"); 32 33 total = source.Count(predicate); 34 if (sortConditions == null || sortConditions.Length == 0) 35 { 36 source = source.OrderBy(m => m.AddDate); 37 } 38 else 39 { 40 int count = 0; 41 IOrderedQueryable<T> orderSource = null; 42 foreach (PropertySortCondition sortCondition in sortConditions) 43 { 44 orderSource = count == 0 45 ? source.OrderBy(sortCondition.PropertyName, sortCondition.ListSortDirection) 46 : orderSource.ThenBy(sortCondition.PropertyName, sortCondition.ListSortDirection); 47 count++; 48 } 49 source = orderSource; 50 } 51 return source != null 52 ? source.Where(predicate).Skip((pageIndex - 1) * pageSize).Take(pageSize) 53 : Enumerable.Empty<T>().AsQueryable(); 54 }
这样,要获取某页数据,只要调用这个扩展方法即可,跟调用系统的扩展方法一样方便(其中total是总记录数)。
1 int total; 2 var pageData = source.Where(m => m.IsDeleted, 4, 20, out total);
下面,我们来实战一下数据查询。
首先,我们要查询的数据将用下面这个类来显示,其中LoginLogCount为当前用户的登录次数,RoleNames为用户拥有的角色名称集合,这两个数据都来源于与Member有关联的其他表。
1 namespace GMF.Demo.Site.Models 2 { 3 public class MemberView 4 { 5 public int Id { get; set; } 6 7 public string UserName { get; set; } 8 9 public string NickName { get; set; } 10 11 public string Email { get; set; } 12 13 public bool IsDeleted { get; set; } 14 15 public DateTime AddDate { get; set; } 16 17 public int LoginLogCount { get; set; } 18 19 public IEnumerable<string> RoleNames { get; set; } 20 } 21 }
为了简化演示操作,引入分页控件MVCPager来处理页面上的分页条的处理。
Controller中代码如下,注意数据获取的查询代码:
1 namespace GMF.Demo.Site.Web.Controllers 2 { 3 [Export] 4 public class HomeController : Controller 5 { 6 [Import] 7 public IAccountSiteContract AccountContract { get; set; } 8 9 public ActionResult Index(int? id) 10 { 11 int pageIndex = id ?? 1; 12 const int pageSize = 20; 13 PropertySortCondition[] sortConditions = new[] { new PropertySortCondition("Id") }; 14 int total; 15 var memberViews = AccountContract.Members.Where(m => true, pageIndex, pageSize, out total, sortConditions).Select(m => new MemberView 16 { 17 UserName = m.UserName, 18 NickName = m.NickName, 19 Email = m.Email, 20 IsDeleted = m.IsDeleted, 21 AddDate = m.AddDate, 22 LoginLogCount = m.LoginLogs.Count, 23 RoleNames = m.Roles.Select(n => n.Name) 24 }); 25 PagedList<MemberView> model = new PagedList<MemberView>(memberViews, pageIndex, pageSize, total); 26 return View(model); 27 } 28 } 29 }
这里虽然使用了MVCPager,但并没有使用她的分页功能。分页处理还是我们自己做的,只是使用了她的单页数据模型类PageList<T>作为视图模型
View代码如下:
1 @using Webdiyer.WebControls.Mvc; 2 @using GMF.Component.Tools; 3 @model PagedList<GMF.Demo.Site.Models.MemberView> 4 @{ 5 ViewBag.Title = "Index"; 6 Layout = "~/Views/Shared/_Layout.cshtml"; 7 } 8 9 <h2>Index</h2> 10 @if (!User.Identity.IsAuthenticated) 11 { 12 @Html.ActionLink("登录", "Login", "Account") 13 } 14 else 15 { 16 <div> 17 用户 @User.Identity.Name 已登录 18 @Html.ActionLink("退出", "Logout", "Account") 19 </div> 20 } 21 <table> 22 <tr> 23 <th>UserName</th> 24 <th>NickName</th> 25 <th>Email</th> 26 <th>IsDeleted</th> 27 <th>AddDate</th> 28 <th>LoginLogCount</th> 29 <th>RoleNames</th> 30 </tr> 31 32 @foreach (var item in Model) { 33 <tr> 34 <td>@Html.DisplayFor(modelItem => item.UserName)</td> 35 <td>@Html.DisplayFor(modelItem => item.NickName)</td> 36 <td>@Html.DisplayFor(modelItem => item.Email)</td> 37 <td>@Html.DisplayFor(modelItem => item.IsDeleted)</td> 38 <td>@Html.DisplayFor(modelItem => item.AddDate)</td> 39 <td style="text-align:center;"> 40 @Html.DisplayFor(modelItem => item.LoginLogCount) 41 </td> 42 <td>@item.RoleNames.ExpandAndToString(",")</td> 43 </tr> 44 } 45 </table> 46 @Html.Pager(Model, new PagerOptions 47 { 48 PageIndexParameterName = "id" 49 })
显示效果如下:
查询执行的SQL语句如下:
1 SELECT 2 [Project2].[Id] AS [Id], 3 [Project2].[UserName] AS [UserName], 4 [Project2].[NickName] AS [NickName], 5 [Project2].[Email] AS [Email], 6 [Project2].[IsDeleted] AS [IsDeleted], 7 [Project2].[AddDate] AS [AddDate], 8 [Project2].[C2] AS [C1], 9 [Project2].[C1] AS [C2], 10 [Project2].[Name] AS [Name] 11 FROM ( SELECT 12 [Limit1].[Id] AS [Id], 13 [Limit1].[UserName] AS [UserName], 14 [Limit1].[NickName] AS [NickName], 15 [Limit1].[Email] AS [Email], 16 [Limit1].[IsDeleted] AS [IsDeleted], 17 [Limit1].[AddDate] AS [AddDate], 18 [Join1].[Name] AS [Name], 19 CASE WHEN ([Join1].[Member_Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1], 20 [Limit1].[C1] AS [C2] 21 FROM (SELECT TOP (20) [Project1].[Id] AS [Id], [Project1].[UserName] AS [UserName], [Project1].[NickName] AS [NickName], [Project1].[Email] AS [Email], [Project1].[IsDeleted] AS [IsDeleted], [Project1].[AddDate] AS [AddDate], [Project1].[C1] AS [C1] 22 FROM ( SELECT [Project1].[Id] AS [Id], [Project1].[UserName] AS [UserName], [Project1].[NickName] AS [NickName], [Project1].[Email] AS [Email], [Project1].[IsDeleted] AS [IsDeleted], [Project1].[AddDate] AS [AddDate], [Project1].[C1] AS [C1], row_number() OVER (ORDER BY [Project1].[Id] ASC) AS [row_number] 23 FROM ( SELECT 24 [Extent1].[Id] AS [Id], 25 [Extent1].[UserName] AS [UserName], 26 [Extent1].[NickName] AS [NickName], 27 [Extent1].[Email] AS [Email], 28 [Extent1].[IsDeleted] AS [IsDeleted], 29 [Extent1].[AddDate] AS [AddDate], 30 (SELECT 31 COUNT(1) AS [A1] 32 FROM [dbo].[LoginLogs] AS [Extent2] 33 WHERE [Extent1].[Id] = [Extent2].[Member_Id]) AS [C1] 34 FROM [dbo].[Members] AS [Extent1] 35 ) AS [Project1] 36 ) AS [Project1] 37 WHERE [Project1].[row_number] > 0 38 ORDER BY [Project1].[Id] ASC ) AS [Limit1] 39 LEFT OUTER JOIN (SELECT [Extent3].[Member_Id] AS [Member_Id], [Extent4].[Name] AS [Name] 40 FROM [dbo].[RoleMembers] AS [Extent3] 41 INNER JOIN [dbo].[Roles] AS [Extent4] ON [Extent4].[Id] = [Extent3].[Role_Id] ) AS [Join1] ON [Limit1].[Id] = [Join1].[Member_Id] 42 ) AS [Project2] 43 ORDER BY [Project2].[Id] ASC, [Project2].[C1] ASC
执行的SQL语句虽然比较复杂,但是确实是按我们的需求来进行最简查询的,比如我们没有查询Member的Password属性,上面就没有Password相关的语句,LoginLog的计数,Roles的Name属性的筛选,也没有涉及该类的其他属性的查询。
为了让大家能第一时间获取到本架构的最新代码,也为了方便我对代码的管理,本系列的源码已加入微软的开源项目网站 http://www.codeplex.com,地址为:
https://gmframework.codeplex.com/
可以通过下列途径获取到最新代码: