前言

  首先对大家表示抱歉,这个系列已经将近一个月没有更新了,相信大家等本篇更新都等得快失望了。实在没办法,由于本人水平有限,写篇博客基本上要大半天的时间,最近实在是抽不出这么长段的空闲时间来写。另外也是一直没想好本篇应该怎样写比较容易理解,于是就一天一天的拖着了。废话不多说,言归正传。

  EF的CodeFirst是个好东西,让我们完全不用考虑数据库端(注意,这里并不是说不需要对数据库知识进行了解),一切工作都可以通过代码来完成。EF是ORM,已经把数据访问操作封装得很好了,可以直接在业务层中使用,那我们为什么还要对其进行那么多封装呢?在我看来,封装至少能带来如下的好处:

  1. 把EF的相关对象封装在数据访问层中,解除了业务层对EF的依赖。
  2. 统一EF的数据操作,以保证业务层使用相同的代码规范
  3. 隐藏EF的敏感配置,降低EF的使用难度

  这里就引入一个问题,应该怎样来进行EF的封装呢,既要保证使用的统一与方便性,又要保持EF的灵便性,否则,封装将变成给业务层设置障碍。下面,主要针对数据查询进对可能出现的误用情况进行分析。

查询问题分析

数据查询应该在哪做

  在EF中,面向对象的数据查询主要提供了两种方式:

  1. TEntity DbSet<TEntity>.Find(params object[] keyValues):针对主键设计的通过主键查找单个实体,会先在EF的本地数据集Local中进行查询,如果没有,再去数据库中查询。
  2. IQueryable<T>、IEnumerable<T>类型的所有数据查询的扩展方法(由于DbSet<T>继承于IQueryable<T>与IEnumerable<T>),如SingleOrDefault,FirstOrDefault,Where等。其中IQueryable<T>的扩展方法会先收集需求,到最后一步再生成相应的SQL语句进行数据查询;而IEnumerable<T>的扩展方法则是在查询的第一步就生成相应的SQL语句获取数据到内存中,后面的操作都是以内存中的数据为基础进行操作的。

  以上两种方式为EF的数据查询提供了极大的自由度,这个自由度是我们在封装的时候需要保持的。但是,在阅读不少人(其中不乏工作了几年的)对EF的封装,设计统一的数据操作接口Repository中关于数据查询的操作中,通常会犯如下几种失误:

  1. 设计了很多GetByName,GetByXX,GetByXXX的操作,这些操作通常并不是所有实体都会用到,只是部分实体的部分业务用到,或者是“估计会用到”。
  2. 定义了按条件查询的SingleOrDefault,FirstOrDefault,Count,GetByPredicate(predicate)等方法,但是对于条件predicate的类型是使用Expression<Func<TEntity, boo>>还是Func<TEntity, bool>很纠结,最后干脆两个都设计,相当于把IQueryable<T>,IEnumerable<T>的方法再过一遍
  3. 定义了获取全部数据的GetAll()方法,但却使用了IEnumerable<TEntity>类型的返回值,明白的同学都知道,这相当于把整个表的数据都加载到内存中,问题很严重,设计者却不知道。

  诸如此类,各种奇葩的查询操作层出不穷,这些操作或者破坏了EF数据查询原有的灵活性,或者画蛇添足

  其实,这么多失误的原因只有一个,设计者忘记了EF是ORM,把EF当作ado.net来使用了。只要记着EF是ORM,以上这些功能已经实现了,就不要去重复实现了。那么以上的问题就非常好解决了,只要:

在数据操作Repository接口中把EF的DbSet<TEntity>开放成一个只读的IQueryable<TEntity>类型的属性提供给业务层作为数据查询的数据源

就可以了。这个数据源是只读的,并且类型是IQueryable<T>,就保证了它只能作为数据查询的数据源,而不像开放了DbSet<T>类型那样可以在业务层中调用EF的内部方法进行增、删、改等操作。另外IQueryable<T>类型保持了EF原有的查询自由性与灵活性,简单明了。这个数据集还可以传递到业务层的各个层次,以实现在哪需要数据就在哪查的灵活性。

循环中的查询陷阱

  EF的导航属性是延迟加载的,延迟加载的优点就是不用到不加载,一次只加载必要的数据,这减少了每次加载的数据量,但缺点也不言自明:极大的增加了数据库连接的次数,比如如下这么个简单的需求:

输出每个用户拥有的角色数量

  根据这个需求,很容易就写出了如下的代码:

  

  遍历所有用户信息,输出每个用户信息中角色(导航属性)的数量。

  上面这段代码逻辑很清晰,看似没有什么问题。我们来分析一下代码的执行过程:

  1. 132行,从IOC容器中获取用户仓储接口的实例,这没什么问题。
  2. 133行,取出所有用户信息(memberRepository.Entities),执行SQL如下:
    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有些复杂,但还是没什么问题

  3. 136行,就开始有问题了,每次循环都会连接一次数据库,执行一次如下查询(最后一个1是用户编号):
    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>的接口了,为什么暴露这个接口,上面也说了很多了。下面,以账户模块为例,我们就来看看怎样把这个查询数据集往上传递。

  首先,不要忘了,我们的项目结构是这样的:

  

  1. 对注入的Repository接口进行保护

    在核心业务实现类(AccountService)中,我们进行了各个相关实体的Repository接口的注入

    这里要注意,实体的Repository接口只能在业务层中使用,以防止开发者在展现层中调用增、删、改等数据操作以实现业务,而不是在业务层中进行业务实现。因而,注入的实体的Repository接口属性可访问性要修改为 protected

  2. 开放查询数据集供展现层使用

    业务层中的Repository接口都设置为 protected 了,那么在展现层无法访问 IEntityRepository.Entities 数据集了,怎样实现展现层的数据的查询呢,很简单,只要在业务接口中把 IEntityRepository.Entities 数据集 再包装成一个IQueryable<T>的查询数据集开发出去,就可以了。

  3. 在业务实现类中进行 IEntityRepository.Entities 数据集的包装:


    经过这样的封装,在业务层中,我们可以使用 IEntityRepository.Entities 数据集 进行数据查询,在展现层中使用业务契约中开放的数据集进行查询。由于开发的数据集仍是IQueryable<T>类型,对EF的查询自由度没有损耗。

查询陷阱的应对方案

  对于前面提到的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]

  相当简洁,这才是我们需要的效果。

匿名对象方案与实体对象方案对比

  匿名对象的方案虽然达到了我们想要的效果,但对比实体对象方案,又有什么不同呢,下面我们来对比一下:

  1. 数据传递性、复用性:
    -匿名对象:基本上属于一次性数据,无法整体传递,无法复用。
    +实体对象:传递性,复用性良好。
  2. 对重构、方法提取的支持:
    -匿名对象:由于数据无法传递,写出的代码很难进行重构,我就普写过几百行代码而无法提取子方法重构的方法。
    +实体对象:数据对代码重构、方法提取支持良好。
  3. 对缓存命中率的影响:
    -匿名对象:数据与具体的业务场景(参数、条件等)密切关联,缓存命中率可能会较低。
    +实体对象:数据易复用,缓存命中率可能会较高。
  4. 不同层次的数据模型自动映射转换(AutoMapper等)
    -匿名对象:属性不定,类型不定,难以转换。
    +实体对象:轻松实现映射转换。
  5. 数据利用率:
    +匿名对象:数据按需获取,利用率高,基本无浪费。
    -实体对象:数据都是整体取出,利用率低,浪费大。
  6. 程序性能影响:
    +匿名对象:容易写出运行高效的代码,性能良好。
    -实体对象:容易写出性能低下的代码。

  通过上面的对比,希望能对方案的选择提供一些参考,至于如何取舍,最终选择什么方案,只能自己根据业务的特点来权衡了,合适用哪个就用哪个。

需求实现

  前面已经说过不少次了,这里在明确的提一次,在这个架构设计中,如果现有查询方法不能满足业务需求,需要添加一个相应的查询功能,你不需要到数据层去进行操作,你只需要:

扩展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 == )
  35. {
  36. source = source.OrderBy(m => m.AddDate);
  37. }
  38. else
  39. {
  40. int count = ;
  41. IOrderedQueryable<T> orderSource = null;
  42. foreach (PropertySortCondition sortCondition in sortConditions)
  43. {
  44. orderSource = count ==
  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 - ) * pageSize).Take(pageSize)
  53. : Enumerable.Empty<T>().AsQueryable();
  54. }

  这样,要获取某页数据,只要调用这个扩展方法即可,跟调用系统的扩展方法一样方便(其中total是总记录数)。

  1. int total;
  2. var pageData = source.Where(m => m.IsDeleted, , , 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 ?? ;
  12. const int pageSize = ;
  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.  
  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. })
  1.  

  显示效果如下:

  

  查询执行的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/

  可以通过下列途径获取到最新代码:

  • 如果你是本项目的参与者,可以通过VS自带的团队TFS直接连接到 https://tfs.codeplex.com:443/tfs/TFS17 获取最新代码
  • 如果你安装有SVN客户端(亲测TortoiseSVN 1.6.7可用),可以连接到 https://gmframework.svn.codeplex.com/svn 获取最新代码
  • 如果以上条件都不满足,你可以进入页面 https://gmframework.codeplex.com/SourceControl/latest 查看最新代码,也可以点击页面上的 Download 链接进行压缩包的下载,你还可以点击页面上的 History 链接获取到历史版本的源代码
  • 如果你想和大家一起学习MVC,学习EF,欢迎加入群:5008599(群发言仅限技术讨论,拒绝闲聊,拒绝酱油,拒绝广告)
  • 如果你想与我共同来完成这个开源项目,可以随时联系我。

系列导航

  1. MVC实用架构设计(〇)——总体设计
  2. MVC实用架构设计(一)——项目结构搭建
  3. MVC实用架构设计(二)——使用MEF应用IOC
  4. MVC实用架构设计(三)——EF-Code First(1):Repository,UnitOfWork,DbContext
  5. MVC实用架构设计(三)——EF-Code First(2):实体映射、数据迁移,重构
  6. MVC实用架构设计(三)——EF-Code First(3):使用T4模板生成相似代码
  7. MVC实用架构设计(三)——EF-Code First(4):数据查询
  8. MVC实用架构设计(三)——EF-Code First(5):二级缓存
  9. MVC实体架构设计(三)——EF-Code First(6):数据更新
  10. 未完待续。。。

MVC实用架构设计(三)——EF-Code First(4):数据查询的更多相关文章

  1. MVC实用架构设计(三)——EF-Code First(5):二级缓存

    前言 今天我们来谈谈EF的缓存问题. 缓存对于一个系统来说至关重要,但是是EF到版本6了仍然没有见到有支持查询结果缓存机制的迹象.EF4开始会把查询语句编译成存储过程缓存在Sql Server中,据说 ...

  2. MVC实用架构设计(三)——EF-Code First(1):Repository,UnitOfWork,DbContext

    前言 终于到EF了,实在不好意思,最近有点忙,本篇离上一篇发布已经一个多星期了,工作中的小迭代告一段落,终于有点时间来继续我们的架构设计了,在这里先对大家表示歉意. 其实这段时间我并不是把这个系列给忘 ...

  3. MVC实用架构设计(三)——EF-Code First(3):使用T4模板生成相似代码

    前言 经过前面EF的<第一篇>与<第二篇>,我们的数据层功能已经较为完善了,但有不少代码相似度较高,比如负责实体映射的 EntityConfiguration,负责仓储操作的I ...

  4. [转]MVC实用架构设计(三)——EF-Code First(3):使用T4模板生成相似代码

    本文转自:http://www.cnblogs.com/guomingfeng/p/mvc-ef-t4.html 〇.目录 一.前言 二.工具准备 三.T4代码生成预热 (一) 单文件生成:Hello ...

  5. MVC 实用架构设计(三)——EF-Code First(5):二级缓存

    一.前言 今天我们来谈谈EF的缓存问题. 缓存对于一个系统来说至关重要,但是是EF到版本6了仍然没有见到有支持查询结果缓存机制的迹象.EF4开始会把查询语句编译成存储过程缓存在Sql Server中, ...

  6. MVC 实用架构设计(〇)——总体设计

    〇.目录 一.前言 二.结构图 三.结构说明 一.前言 一直以来都想写这个系列,但基于各种理由(主要是懒惰),迟迟没有动手.今天,趁着周末的空档,终于把系列的目录公布出来了,算是开个头,也给自己一个坚 ...

  7. MVC实用架构设计:总体设计

    http://developer.51cto.com/art/201309/410166.htm

  8. MVC实用构架设计(三)——EF-Code First(6):数据更新最佳实践

    前言 最近在整理EntityFramework数据更新的代码,颇有体会,觉得有分享的价值,于是记录下来,让需要的人少走些弯路也是好的. 为方便起见,先创建一个控制台工程,使用using(var db ...

  9. EF和MVC系列文章导航:EF Code First、DbContext、MVC

    对于之前一直使用webForm服务器控件.手写ado.net操作数据库的同学,突然来了EF和MVC,好多新概念泉涌而出,的确犹如当头一棒不知所措.本系列文章可以帮助新手入门并熟练使用EF和MVC,有了 ...

随机推荐

  1. Shader实例:序列帧动画

    效果: 序列帧图片网上随便找的,质量不是很好,重点不是它,不要在意. 思路: 1.之前都是在一张面片上直接映射一张纹理,IN.uv的范围是0~1 现在要映射一张纹理上的一小块区域,就要用这块区域的uv ...

  2. eclipse常见问题

    使用eclipse进入断点,当弹出"Confir Perspective Switch"视图时,选择"Yes".之后每次进入断点都会自动切换到debug视图. ...

  3. [转]Android Studio 里搭建自动化测试框架Robotium

    Android的自动化测试框架可选择的不多,后来选了Robotium(https://code.google.com/p/robotium/),它的语法及易用性挺像我们用在iOS里的KIF. 官方文档 ...

  4. swfupload纠结bug总结

    上传控件传到客户端的信息在IE7下乱码: 服务端 HttpUtility.UrlEncode,客户端 decodeURIComponent 上传大文件报404错: 用fiddler截取发现提示: 最可 ...

  5. Java 集合类的特性

    ArrayList: 元素单个,效率高,多用于查询  Vector: 元素单个,线程安全,多用于查询 LinkedList: 元素单个,多用于插入和删除  HashMap: 元素成对,元素可为空  H ...

  6. Kafka设计解析(三)- Kafka High Availability (下)

    本文转发自Jason’s Blog,原文链接 http://www.jasongj.com/2015/06/08/KafkaColumn3 摘要 本文在上篇文章基础上,更加深入讲解了Kafka的HA机 ...

  7. sublime Text3及其插件的使用

    参考:Sublime Text 3 新手上路:必要的安裝.設定與基本使用教學 Sublime Text 相信是許多開發人員人心目中的最愛,然而對一個 Sublime Text 3 的新手來說,有什麼是 ...

  8. Mac上开启Web服务

    $ sudo apachectl start$ httpd -v /Library/WebServer/Documents

  9. 关于object和embed

    最近发现很久之前写的demo 在FF上有兼容问题, 主要发生在如下: 由于时间太久,自己都有点搞不清怎么写的,尤其是object标签,今天查了很多资料,总结一下object标签 在网页中正常显示fla ...

  10. Extjs扩展:封装Plupload

    关于Plupload Plupload是一个web文件上传组件,支持通过HTML5.Silverlight.Flash或者普通的form来上传文件,提供了过滤文件类型.设置上传文件大小.上传进度.针对 ...