EFCore中使用Where查询时进行多个字段匹配
背景
在EFCore中我们经常有这样一种需求那就是我们需要同时匹配一个实体中的多个字段,这个在EFCore中一般的代码都是匹配特定的字段,如果遇到这种情况我们该如何准确进行匹配呢?这个时候就需要用到我们今天提到的扩展方法。
查询实例
在下面的例子中toAddVehicleOrderPlans是我们前面已经查询并放到内存中的一个集合对象,这里我们将这个集合中的DealerId、YearOfPlan、WeekOfPlan这几个对象放到一个匿名集合中,然后用这个集合来匹配_weeklyOrderPlanRepository中的对象,这里我们用到了一个WhereByMultiOr的扩展方法,这个方法能够将最终的Linq查询转变成我们想要的结果。
var filterPlans = toAddVehicleOrderPlans.Select(r => new { r.DealerId, r.YearOfPlan, r.WeekOfPlan }).ToArray(); var toDeleteEntities = await _weeklyOrderPlanRepository.GetAll() .WhereByMultiOr(filterPlans, (d, f) => d.DealerId == f.DealerId && d.YearOfPlan == f.YearOfPlan && d.WeekOfPlan == f.WeekOfPlan) .ToListAsync();
过程分析
我们首先来看看我们是怎么来定义这个方法的。
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; namespace Sunlight.EFCore.Extensions { /// <summary> /// 拼接生成多个 Or 的Where语句 /// </summary> public static class WhereByMultiOrExtend { /// <summary> /// 多个or条件过滤, 如 Boms.Where(b=> (b.Code == "a" and b.Name == "A") or (b.Code == "b" and b.Name == "B")) /// <para> filters 可以是查询出来的。</para> /// <code> /// var filters = new Dictionary<string, string>(); /// filters.Add("C70GCCC4A004YAC", "1.0"); /// filters.Add("C30DABC1Q003KAF", "1.0"); /// ObjectContext.DomEditions.WhereByMultiOr(filters.ToList(), (b, f) => b.ProductCode == f.Key && b.BatchCode == f.Value); /// </code> /// </summary> /// <typeparam name="T">被过滤的数据类型</typeparam> /// <typeparam name="TFilter">条件的类型</typeparam> /// <param name="entitySet">数据集</param> /// <param name="filters">过滤条件集合</param> /// <param name="predicate"></param> /// <returns></returns> public static IQueryable<T> WhereByMultiOr<T, TFilter>(this IQueryable<T> entitySet, IEnumerable<TFilter> filters, Expression<Func<T, TFilter, bool>> predicate) { var innerFilters = filters.ToArray(); if (innerFilters == null || innerFilters.Length == 0) throw new ArgumentOutOfRangeException(nameof(filters)); //条件参数数据常量化,所以参数只保留 被过滤的数据 var pe = predicate.Parameters.First(); var orElseExpressions = innerFilters.Select(filter => new ParameterModifier<TFilter>().Modify(predicate, filter)) .ToList(); var predicateBody = orElseExpressions.First(); if (orElseExpressions.Count > 1) { for (var i = 1; i < orElseExpressions.Count; i++) { predicateBody = Expression.OrElse(predicateBody, orElseExpressions[i]); } } var whereCallExpression = Expression.Call( typeof(Queryable), "Where", new[] { typeof(T) }, entitySet.Expression, Expression.Lambda<Func<T, bool>>(predicateBody, pe) ); return entitySet.Provider.CreateQuery<T>(whereCallExpression); } private class ParameterModifier<TFilter> : ExpressionVisitor { private TFilter _localParam; /// <summary> /// 将predicate中用到的F里的数据作为常量放到表达式里。如 TF 是 KeyValueItem, key="abc", value = "1.0", /// <para> 则表达式 (b, f) => b.ProductCode == f.Key && b.BatchCode == f.Value)</para> /// 变成 (b.ProductCode == "abc" && b.BatchCode == "1.0") /// </summary> /// <param name="expression">表达式</param> /// <param name="param">条件对象</param> /// <returns></returns> public Expression Modify(Expression expression, TFilter param) { _localParam = param; return Visit(expression); } protected override Expression VisitBinary(BinaryExpression node) { var left = Visit(node.Left); var right = Visit(node.Right); string memberName; if (CheckForParameter(left)) left = Expression.Constant(_localParam); else if (CheckForProperty(left, out memberName)) left = Expression.Constant(_localParam.GetType().GetProperty(memberName).GetValue(_localParam, null)); if (CheckForParameter(right)) right = Expression.Constant(_localParam); else if (CheckForProperty(right, out memberName)) right = Expression.Constant(_localParam.GetType().GetProperty(memberName) .GetValue(_localParam, null)); // right为常量的时候,需要转换成和left一样的类型,比如 int 转换成 Nullable<int> right = Expression.Convert(right, left.Type); return Expression.MakeBinary(node.NodeType, left, right); } /// <summary> /// 常量访问判断 /// </summary> /// <param name="e"></param> /// <returns></returns> private static bool CheckForParameter(Expression e) { return e is ParameterExpression; } /// <summary> /// 属性访问判断 /// </summary> /// <param name="e"></param> /// <param name="memberName"></param> /// <returns></returns> private static bool CheckForProperty(Expression e, out string memberName) { memberName = string.Empty; if (!(e is MemberExpression me) || me.Expression.Type != typeof(TFilter)) return false; memberName = me.Member.Name; return true; } /// <summary> /// 因为要去掉参数,这里只取Body /// </summary> /// <typeparam name="T"></typeparam> /// <param name="node"></param> /// <returns></returns> protected override Expression VisitLambda<T>(Expression<T> node) { return Visit(node.Body); } } } }
这个里面的核心是如何按照我们的想法拼接自定义的Expression,这里我们创建泛型ParameterModifier<TFilter> 继承自 ExpressionVisitor,然后最核心的就是要重写这个基类里面的VisitBinary方法,后面为了方便进行测试我们使用了XUnit的框架对我们写的这个类进行测试,测试用例覆盖越全面整个方法就越准确,我们来看看我们的测试用例,这里以WhereByMultiOr_FilterByStringAndInt_Success这个单元测试为例,我们来看看这些方法都生成了些什么?这个方法中的核心是创建ParameterModifier的实例,然后调用Modify方法,所以这个Modify方法最核心的功能,就像注释说的一样:表达式 (b, f) => b.ProductCode == f.Key && b.BatchCode == f.Value)转变成
(b.ProductCode == "abc" && b.BatchCode == "1.0")这个是最后转成SQL的关键,其它部分的内容可以通过调试每一个方法来看看生成了些什么,这样就能够加深对整个过程的理解。
using System; using System.Collections.Generic; using System.Linq; using Shouldly; using Sunlight.EFCore.Extensions; using Xunit; namespace Sunlight.Framework.EFCore.Tests { public class WhereByMultiOrExtend_Tests { private class Student { public int Id { get; set; } public string Code { get; set; } public string Name { get; set; } public int Age { get; set; } public int? Weight { get; set; } } private IQueryable<Student> GenerateStudents() { var entitySet = new List<Student>() { new Student { Id = 1, Code = "Stu01", Name = "王二", Age = 22, Weight = 110 },new Student { Id = 2, Code = "Stu02", Name = "张三", Age = 22, Weight = 120 }, new Student { Id = 3, Code = "Stu03", Name = "李四", Age = 24, Weight = 130 } }; return entitySet.AsQueryable(); } [Fact] public void WhereByMultiOr_EmptyFilters_ThrowException() { // Arrange var students = GenerateStudents(); var filters = Enumerable.Empty<Student>(); // Assert Assert.Throws<ArgumentOutOfRangeException>( // Act () => students.WhereByMultiOr(filters, (s, f) => s.Code == f.Code && s.Name == f.Name) ); } [Fact] public void WhereByMultiOr_FilterByStringAndInt_Success() { // Arrange var students = GenerateStudents(); var filters = new[] { new {Name = "王二", Age = 22} }; // Act var results = students.WhereByMultiOr(filters, (s, f) => s.Name == f.Name && s.Age == f.Age); // Assert results.Count().ShouldBe(1); } [Fact] public void WhereByMultiOr_FilterByIntAndNullableInt_Success() { // Arrange var students = GenerateStudents(); var filters = new[] { new {Wight = (int?)120, Age = 22}, new {Wight = (int?)130, Age = 24} }; // Act var results = students.WhereByMultiOr(filters, (s, f) => s.Weight == f.Wight && s.Age == f.Age); // Assert results.Count().ShouldBe(2); } } }
通过这些单元测试用例我们就能够准确去理解代码中每一行的意义然后生成正确SQL语句,最后我们回到一开始我们提出的那个问题,这段代码我们看看在实际的SQL语句是否符合预期效果。
SELECT d.Id, d.BranchId, d.Code, d.ConfirmationTime, d.ConfirmorId, d.ConfirmorName, d.CreateTime, d.CreatorId, d.CreatorName, d.DealerCode, d.DealerId, d.DealerName, d.EndTime, d.ModifierId, d.ModifierName, d.ModifyTime, d.MonthOfPlan, d.Remark, d.RowVersion, d.StartTime, d.Status, d.TotalAmount, d.Type, d.VehicleSalesOrgCode, d.VehicleSalesOrgId, d.VehicleSalesOrgName, d.VehicleWarehouseCode, d.VehicleWarehouseId, d.VehicleWarehouseName, d.WeekOfPlan, d.YearOfPlan FROM VehicleOrderPlan d WHERE (((((d.DealerId = 44) AND (d.YearOfPlan = 2020)) AND (d.WeekOfPlan = 37)) OR (((d.DealerId = 44) AND (d.YearOfPlan = 2020)) AND (d.WeekOfPlan = 38))) OR (((d.DealerId = 44) AND (d.YearOfPlan = 2020)) AND (d.WeekOfPlan = 39))) OR (((d.DealerId = 44) AND (d.YearOfPlan = 2020)) AND (d.WeekOfPlan = 40))