使用内部集合创建 where 谓词的动态查询

Posted

技术标签:

【中文标题】使用内部集合创建 where 谓词的动态查询【英文标题】:Dynamic query for creating where predicate with inner Collection 【发布时间】:2018-09-22 03:15:25 【问题描述】:

我正在为我的 MVC EF 应用程序创建搜索功能。我正在使用动态查询创建它。并按照这个方法https://www.codeproject.com/Articles/493917/Dynamic-Querying-with-LINQ-to-Entities-and-Express

它用于为实体的boolstring 字段创建谓词。我的应用程序中的主要实体是Applicant

EDMX Applicant 正在关注

     public partial class Applicant
    

      public Applicant()
       
         this.ApplicantEducations = new HashSet<ApplicantEducation>();
         this.ApplicantSkills = new HashSet<ApplicantSkill>();
         this.Applications = new HashSet<Application>();
         this.Experiences = new HashSet<Experience>();
        

    public int Id  get; set; 
    public string Name  get; set; 
    public string Description  get; set; 
    public byte[] CV_Upload1  get; set; 
    public string CV_Upload2  get; set; 
    public string email  get; set; 
    public string password  get; set; 
    public Nullable<System.DateTime> DOB  get; set; 

   virtual ICollection<ApplicantEducation> ApplicantEducations  get; set; 
   virtual ICollection<ApplicantSkill> ApplicantSkills  get; set; 
   virtual ICollection<Application> Applications  get; set; 
   virtual ICollection<Experience> Experiences  get; set; 

我想搜索,即在类型为 Institute 的申请人教育中归档的机构名称。申请人可以有一个或多个申请人教育对象。

以下是我的申请者教育的 EDMX 课程

   public partial class ApplicantEducation

    public int id  get; set; 
    public Nullable<int> ApplicantId  get; set; 
    public Nullable<int> InstituteId  get; set; 
    public Nullable<int> EducationLevelId  get; set; 
    public Nullable<bool> IsComplete  get; set; 
    public Nullable<System.DateTime> DateStart  get; set; 
    public Nullable<System.DateTime> DateEnd  get; set; 
    public Nullable<short> GPA  get; set; 

    public virtual EducationLevel EducationLevel  get; set; 
    public virtual Institute Institute  get; set; 
    public virtual Applicant Applicant  get; set; 

而我的研究所实体类是这样的

public class Institute
  
         public int Id  get; set; 
         public string Name  get; set; 

  

因此,用户将通过指定机构名称进行搜索,所有申请人都将从该机构获得教育。

正如我上面提到的链接。下面以字符串字段谓词构建为例进行演示

     private static Expression<Func<TDbType, bool>> ApplyStringCriterion<TDbType,
        TSearchCriteria>(TSearchCriteria searchCriteria, PropertyInfo searchCriterionPropertyInfo,
        Type dbType, MemberInfo dbFieldMemberInfo, Expression<Func<TDbType, bool>> predicate)
    
        // Check if a search criterion was provided
        var searchString = searchCriterionPropertyInfo.GetValue(searchCriteria) as string;
        if (string.IsNullOrWhiteSpace(searchString))
        
            return predicate;
        
        // Then "and" it to the predicate.
        // e.g. predicate = predicate.And(x => x.firstName.Contains(searchCriterion.FirstName)); ...
        // Create an "x" as TDbType
        var dbTypeParameter = Expression.Parameter(dbType, @"x");
        // Get at x.firstName
        var dbFieldMember = Expression.MakeMemberAccess(dbTypeParameter, dbFieldMemberInfo);
        // Create the criterion as a constant
        var criterionConstant = new Expression[]  Expression.Constant(searchString) ;
        // Create the MethodCallExpression like x.firstName.Contains(criterion)
        var containsCall = Expression.Call(dbFieldMember, StringContainsMethod, criterionConstant);
        // Create a lambda like x => x.firstName.Contains(criterion)
        var lambda = Expression.Lambda(containsCall, dbTypeParameter) as Expression<Func<TDbType, bool>>;
        // Apply!
        return predicate.And(lambda);
    

上面的代码用于为包含在主实体类 (Applicant) 中的简单字符串字段构建谓词。但是申请人也有申请人教育集合,所以我的问题是如何为linq的where子句(方法)创建一个动态查询(谓词),这样当用户搜索机构名称时,所有申请人都会以相同的教育被检索。

我的搜索条件如下,

  public class SearchCriteriaVM

    public int Id  get; set; 
    public string Name  get; set; 
    public DateTime? DOB  get; set;      
    public string Description  get; set; 

    public ICollection<Models.ApplicantEducationVM> ApplicantEducations  get; set; 
    public ICollection<Models.ExperienceVM> Experiences  get; set; 
    public ICollection<Models.ApplicantSkillsVM> ApplicantSkills  get; set; 

    public ICollection<Models.ApplicationsVM> Applications  get; set; 




我有点迷茫,这怎么可能。

谢谢

【问题讨论】:

有可能(必须生成类似outer.Collection.Any(inner_predicate) 的内容),但不确定您将如何指定标准,因为您所遵循的示例是从一个简单的平面对象中获取标准信息。跨度> @IvanStoev 我已经用标准更新了我的问题 你已经在这里回答了这个问题:请参考这个链接***.com/questions/25508595/… @SwetaNair 好的,但是如果你在这里回答并解释,那么我可以接受。 【参考方案1】:

在您的情况下,我们需要的基本内容是使用 EF 的动态查询构建器。即一个基本的“匹配”方法,它包含 IQueryable 格式的数据、搜索词和过滤记录的属性。 “Match”方法是我们需要在代码中使用的方法。

 public static IQueryable<T> Match<T>(
    IQueryable<T> data,
    string searchTerm,
    IEnumerable<Expression<Func<T, string>>> filterProperties)

    var predicates = filterProperties.Select(selector =>
            selector.Compose(value => 
                value != null && value.Contains(searchTerm)));
    var filter = predicates.Aggregate(
        PredicateBuilder.False<T>(),
        (aggregate, next) => aggregate.Or(next));
    return data.Where(filter);

要构建这个表达式方法,我们需要一个Compose方法,以便它可以接受需要搜索的参数。

public static Expression<Func<TFirstParam, TResult>>
    Compose<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)

    var param = Expression.Parameter(typeof(TFirstParam), "param");

    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], newFirst);

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);

这将组成并返回一个 lambda 表达式,但要构建此方法,我们需要“Replace”扩展方法。顾名思义,此方法会将一个表达式的所有实例替换为另一个。

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)

    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);

internal class ReplaceVisitor : ExpressionVisitor

    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    
        this.from = from;
        this.to = to;
    
    public override Expression Visit(Expression node)
    
        return node == from ? to : base.Visit(node);
    

回到实际的“匹配”方法,我们需要一个谓词构建器来帮助我们呈现与搜索相关的 AND、OR 查询。

因此,谓词构建器将如下所示:

 public static class PredicateBuilder

    public static Expression<Func<T, bool>> True<T>()  return f => true; 
    public static Expression<Func<T, bool>> False<T>()  return f => false; 

    public static Expression<Func<T, bool>> Or<T>(
        this Expression<Func<T, bool>> expr1,
        Expression<Func<T, bool>> expr2)
    
        var secondBody = expr2.Body.Replace(
            expr2.Parameters[0], expr1.Parameters[0]);
        return Expression.Lambda<Func<T, bool>>
              (Expression.OrElse(expr1.Body, secondBody), expr1.Parameters);
    

    public static Expression<Func<T, bool>> And<T>(
        this Expression<Func<T, bool>> expr1,
        Expression<Func<T, bool>> expr2)
    
        var secondBody = expr2.Body.Replace(
            expr2.Parameters[0], expr1.Parameters[0]);
        return Expression.Lambda<Func<T, bool>>
              (Expression.AndAlso(expr1.Body, secondBody), expr1.Parameters);
    

因此,我们需要一个 Match 方法,该方法将根据您的要求运行。

如果您需要任何进一步的帮助,请告诉我,具体取决于您的模型结构。

【讨论】:

基本上我使用SearchCriteria而不是搜索词作为字符串。这个Search Criteria是一个恰好包含属性和集合的复杂对象。您可以在问题中看到标准类。如果您尝试在代码中使用它,即控制器的操作方法来获取结果,您会得到更好的主意 搜索词是否基于任一属性?还是会在所有属性上?所有可观察的集合都将被搜索,属性将是搜索词。是这样吗? 它将搜索全部或部分或任何属性或属性。例如,用户可以只指定一个ApplicantEducation 或者可以指定所有教育。所以它不是那么简单。在您的回答中,您可能必须为Any() 实施扩展方法>查看@IvanStoev 评论【参考方案2】:

您可以使用以下方法在 Lambda 表达式中创建 Dynamic Where 子句

public ActionResult GetRecords(int? classId, string name, bool isAll = false)

    var allRecords = repository.Students;

    if (!isAll)
    
        //Retrieve active records only
        allRecords = allRecords.Where(m => m.StatusId == 1);
    
    if (!string.IsNullOrEmpty(name))
    
        allRecords = allRecords.Where(m => m.Name.StartsWith(name));
    
    if (classId.HasValue)
    
        allRecords = allRecords.Where(m => m.ClassId == classId);
    
    // other stuff

同样,可以应用以下方法,以便仅检索以 “query” 参数值开头的记录,如果 “query” 参数值则检索所有记录为空:

IQueryable<StudentViewModel> students = repository.Students.Select(m => 
    new StudentViewModel

    Id = m.Id,
    Name = m.Name + " " + m.Surname
);
if (!string.IsNullOrEmpty(query))

    students = students.Where(m => m.Name.StartsWith(query));

或“性能不佳”的另一种方式:

.Where(m => string.IsNullOrEmpty(query) || m.Name.StartsWith(query));

希望这会有所帮助...

【讨论】:

请参阅我添加到我的问题中的链接并阅读您将更好地了解我想要做什么。顺便说一句,where 扩展方法确实在调用时执行 sql 或延迟到ForEaching? 你能创建一个检测搜索实体的触发器值吗?您还可以使用合并多个实体的 View(以 dB 为单位)或 ViewModel,然后从多个实体中获取所有过滤值?您还可以在 EF 中使用原始查询,如 Advanced Entity Framework 6 Scenarios for an MVC 5 Web Application (12 of 12) 中所述。

以上是关于使用内部集合创建 where 谓词的动态查询的主要内容,如果未能解决你的问题,请参考以下文章

如何动态创建 CriteriaQuery

mybatis 动态SQL

创建没有下推谓词的动态框架问题

MyBatis 动态 SQL 之通用 where

三:动态SQL

从选项(来自 rds - mysql)创建动态框架,提供带有 where 子句的自定义查询