阅读本文你的收获

  1. 了解EF Core在使用上应该要注意的地方
  2. 学习EF Core的优化技术,如禁用跟踪、IQueryable接口等

一、问题描述

Entity Framework Core(以下简称EF)是微软自家出品的大型O/RM框架,使用EF Core,我们可以在C#中用操作对象的方式来访问和操作数据库,它可以帮助我们把对C#对象的增删改查,转换成对应的关系型数据库的SQL语句,同时可以把从数据库返回的数据转换成C#的对象,并可以对对象的状态进行跟踪,只要状态发生改变,它就可以帮助我们生成对应的update、insert或者delete的SQL命令。使用EF可以大大提高程序员的开发效率。

但是使用EF的时候,也有不少要注意的地方。比如说EF操作数据库的性能肯定没有直接用ADO.NET操作SQL语句的性能高;另外,如果使用不当,则会导致内存占用过高,系统性能下降的问题等等。

如果你的系统对性能要求很高,我们就必须知道怎么去规避这些问题,在使用EF Core作为ORM时,你必须要注意以下几个方面:

  • IEnumerable和IQueryable两种接口要充分理解,区别使用
  • EF默认自带实体跟踪机制,只读查询用非跟踪式查询可以提高效率。用AsNoTracking方法。
  • EF对批量操作的支持不太高效,所以像批量添加、删除、修改等操作的执行效率,要注意分析与改进。
  • 对于一些复杂查询,EF帮我们生成的SQL语句有时是低效的,这时可以让EF直接执行原生SQL语句,或者改用ADO.NET方式去执行。(关于这一点,请参考 EFCore基础之如何执行原生SQL语句

今天先来学习一下IEnumerable和IQueryable接口。

二、理解IEnumerable和IQueryable接口

LINQ标准查询运算符是依靠一组 来实现的。而这些扩展方法分别在System.Linq.Enumerable和System.Linq.Queryable这两个静态类中定义。比如:Where()、Select()、Count()、Any()等方法。

以Where扩展方法为例,在System.Linq.Enumerable类中,是这样定义的,它操作的是IEnumerable< TSource > 类型的集合数据,参数是Func< T, bool >委托类型。

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, 
         Func<TSource, bool> predicate);

在System.Linq.Queryable类中,Where扩展方法是这样定义的,它操作的是IQueryable< TSource >类型的数据,参数一个Expression< T,bool >表达式树类型。

public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source,
        Expression<Func<TSource, bool>> predicate);

事实上,IEnumerable是可枚举接口,集合类型如List、Dictionary等都是继承了该接口,它还有一个泛型的版本IEnumerable< T >, 实现了该接口的对象都是集合对象,可以使用foreach接口进行遍历。

对于IEnumerable接口的LINQ扩展方法,可以实现在内存中对这些集合对象进行LINQ查询。使用IEnumerable的例子:

List<Department> data = new List<Department>(); //实例化一个空集合
data.Add(new Department { Id=Guid.NewGuid(), DepName="部门1" });
data.Add(new Department { Id=Guid.NewGuid(), DepName="部门2" });

//对集合做一个LINQ查询,是在内存中操作的
IEnumerable<Department>  result = data.Where(x => x.DepName.Contains("1"));

//result是一个可枚举对象,所以可以用foreach进行遍历
foreach (var item in result) 
{
	Console.WriteLine($"{item.Id}数据是:{item.DepName}");
}

IQueryable继承了IEnumerable,IQueryable是可查询接口,采用这个接口去调用LINQ的扩展方法,则可以根据用户指定的条件表达式生成对应的带筛选条件的SQL语句,但是并没有立即执行SQL语句,所以可以继续对这个可查询的对象拼接各种筛选条件、排序、分组等操作,等到执行ToList、Count等方法的时候,Linq to SQL引擎会把表达式树转化成相应的SQL在数据库服务器上执行,再将结果加载到内存中来。这也是Linq的延迟加载核心思想所在,在很复杂的操作下可能比较慢了,时间换空间 以下是使用IQueryable的例子:

/// <summary>
/// 获取车辆分页列表
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<PageResponseDto<VehicleProductDto>> GetListAsync(QueryVehicleProductDto input)
{
	//虽然用了where方法,但是并没有立即执行SQL语句
    IQueryable<VehicleProductInfo> query = from vendor in _vehicleProductRepository.Table
                                                where (string.IsNullOrEmpty(input.ItemBrand) || vendor.ItemBrand.Contains(input.ItemBrand))
                                                orderby vendor.ItemBrand
                                                select vendor;

    PageResponseDto<VehicleProductDto> result = new ();

    //执行分页查询
    List<VehicleProductInfo> pageList = await query
                                        .Skip((input.PageIndex - 1) * input.PageSize) 
                                        .Take(input.PageSize) //继续拼接 分页的参数,但不执行SQL语句
                                        .ToListAsync();       //调用ToList方法则执行SQL语句

    result.PagedList = pageList.MapToList<VehicleProductDto>(); //通过AutoMapper转换成VehicleProductDto
    result.TotalCount = query.Count();                         //调用Count方法则执行SQL语句

    return result;
}

三、两者的比较总结

在应用到IEnumberable 和IQueryable两个接口时,代码往往很相似,从而造成了很多困惑,然后从上面分析可以看出,两者还是有很大的区别的,各自都有自己特定的使用场景。

下面是IEnumberable和IQueryable的对比:

错误案例演示

//根据输入条件,获取用户分页列表
public PageResponseModel<User> GetUserList(UserPageRequestDto input)
{
    using MyDbContext db = new MyDbContext();
    //过早地ToList了,加载全部数据进内存
    var query = db.Set<User>().AsQueryable().ToList();
    //拼接查询条件
    if (string.IsNullOrEmpty(input.Name))
    {
        query = query.Where(x => x.UName.StartsWith(input.Name)).ToList();
    }
    PageResponseModel<User> pageList = new ();
    pageList.TotalCount = query.Count();
    pageList.PageList = query.OrderBy(x=>x.CreateTime)
                       .Skip((input.PageIndex-1)*input.PageSize)
                       .Take(input.PageSize)
                       .ToList();

    return pageList;
}

以上案例中,因为所有对于IEnumerable的过滤、排序、分组、聚合等操作,都是在内存中进行的,所以在EF Core中使用LINQ查询的时候,如果你不加任何的筛选条件,马上ToList(),然后再进行排序,分组、筛选的话,那也就是说把所有的数据不管用不用得到,都从数据库加载到内存中,只是在内存中进行过滤和排序操作,这样做虽然性能很高,但很消耗内存。属于用“空间换时间”,所以要谨慎使用。

小结

说了这么多,这两者怎么区分使用呢?——操作本地数据源用IEnumerable,操作远程数据源用IQueryable。IQueryable是负责生成SQL语句的,但并不马上执行,可以继续拼接各种的条件到SQL语句中之后再去服务器上执行,占用内存空间小,但是如果查询很复杂,就会比较慢。
IEnumerable是对任意类型的集合都能操作的,不限于是数据库还是一般的数组还是List集合,因为在内存中操作,所以性能高,但是如果你把数据库中的数据全加载进内存来处理,那会很耗内存,导致严重问题。


如果本文对你有帮助的话,请点赞+评论+关注,或者转发给需要的朋友。

12-26 06:44