阅读本文你的收获
- 了解EF Core在使用上应该要注意的地方
- 学习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集合,因为在内存中操作,所以性能高,但是如果你把数据库中的数据全加载进内存来处理,那会很耗内存,导致严重问题。
如果本文对你有帮助的话,请点赞+评论+关注,或者转发给需要的朋友。