programing

Entity Framework의 OR 조건을 포함하는 동적 쿼리

skycolor 2023. 7. 16. 13:12
반응형

Entity Framework의 OR 조건을 포함하는 동적 쿼리

나는 데이터베이스를 검색하고 사용자가 엔티티 프레임워크를 사용하여 동적 쿼리를 생성하는 SO 질문과 마찬가지로 모든 기준(가능한 약 50개)을 동적으로 추가할 수 있는 응용 프로그램을 만들고 있습니다.현재 각 기준을 확인하는 검색을 수행하고 있으며, 검색이 비어 있지 않으면 쿼리에 추가합니다.

C#

var query = Db.Names.AsQueryable();
  if (!string.IsNullOrWhiteSpace(first))
      query = query.Where(q => q.first.Contains(first));
  if (!string.IsNullOrWhiteSpace(last))
      query = query.Where(q => q.last.Contains(last));
  //.. around 50 additional criteria
  return query.ToList();

이 코드는 sql server에서 다음과 유사한 것을 생성합니다(이해하기 쉽게 단순화했습니다).

SQL

SELECT
    [Id],
    [FirstName],
    [LastName],
    ...etc
FROM [dbo].[Names]
WHERE [FirstName] LIKE '%first%'
  AND [LastName] LIKE '%last%'

나는 이제 엔티티 프레임워크를 통해 C#을 사용하여 다음 SQL을 생성하지만 AND 대신 OR을 사용하여 동적으로 기준을 추가할 수 있는 기능을 유지하려고 합니다.

SQL

SELECT
    [Id],
    [FirstName],
    [LastName],
    ...etc
  FROM [dbo].[Names]
WHERE [FirstName] LIKE '%first%'
  OR [LastName] LIKE '%last%' <-- NOTICE THE "OR"

일반적으로 기준은 쿼리에 대해 2~3개의 항목보다 크지 않지만 하나의 거대한 쿼리로 결합하는 것은 선택사항이 아닙니다.저는 concat, union, inters를 시도해 보았는데 그들은 모두 쿼리를 복제하고 UNION에 가입합니다.

엔티티 프레임워크를 사용하여 동적으로 생성된 쿼리에 "OR" 조건을 추가하는 간단하고 깨끗한 방법이 있습니까?

내 솔루션으로 편집 - 2015년 9월 29일

이 글을 올린 이후로, 저는 이것이 약간의 관심을 받고 있다는 것을 알게 되었고, 그래서 저는 제 솔루션을 올리기로 결정했습니다.

// Make sure to add required nuget
// PM> Install-Package LinqKit

var searchCriteria = new 
{
    FirstName = "sha",
    LastName = "hill",
    Address = string.Empty,
    Dob = (DateTime?)new DateTime(1970, 1, 1),
    MaritalStatus = "S",
    HireDate = (DateTime?)null,
    LoginId = string.Empty,
};

var predicate = PredicateBuilder.False<Person>();
if (!string.IsNullOrWhiteSpace(searchCriteria.FirstName))
{
    predicate = predicate.Or(p => p.FirstName.Contains(searchCriteria.FirstName));
}

if (!string.IsNullOrWhiteSpace(searchCriteria.LastName))
{
    predicate = predicate.Or(p => p.LastName.Contains(searchCriteria.LastName));
}

// Quite a few more conditions...

foreach(var person in this.Persons.Where(predicate.Compile()))
{
    Console.WriteLine("First: {0} Last: {1}", person.FirstName, person.LastName);
}

당신은 아마도 당신이 where 문의 AND와 OR을 더 쉽게 제어할 수 있도록 해주는 술어 작성기와 같은 것을 찾고 있을 것입니다.

SQL 문자열처럼 WHERE 절을 제출할 수 있는 Dynamic Linkq도 있으며 이를 WHERE에 대한 올바른 술어로 구문 분석합니다.

LINQKit와 해당 Predicate Builder는 상당히 다양하지만 몇 가지 간단한 유틸리티(각각 다른 식 조작 작업의 기초가 될 수 있음)를 사용하여 보다 직접적으로 이 작업을 수행할 수 있습니다.

첫째, 범용 식 바꾸기:

public class ExpressionReplacer : ExpressionVisitor
{
    private readonly Func<Expression, Expression> replacer;

    public ExpressionReplacer(Func<Expression, Expression> replacer)
    {
        this.replacer = replacer;
    }

    public override Expression Visit(Expression node)
    {
        return base.Visit(replacer(node));
    }
}

다음으로, 주어진 식에서 한 매개변수의 사용을 다른 매개변수로 바꾸는 간단한 유틸리티 방법:

public static T ReplaceParameter<T>(T expr, ParameterExpression toReplace, ParameterExpression replacement)
    where T : Expression
{
    var replacer = new ExpressionReplacer(e => e == toReplace ? replacement : e);
    return (T)replacer.Visit(expr);
}

서로 다른 두 식의 람다 매개변수는 이름이 동일하더라도 실제로는 서로 다른 매개변수이기 때문에 이 작업이 필요합니다.예를 들어, 당신이 끝내길 원한다면,q => q.first.Contains(first) || q.last.Contains(last) 다음에 그음에다.qq.last.Contains(last)정확히 같아야 합니다. q람다 식의 시작 부분에 제공됩니다.

다음은 범용 목적이 필요합니다.Join에 가입할 수 있는Func<T, TReturn> 식과 식 style 람다 식을 사용합니다.

public static Expression<Func<T, TReturn>> Join<T, TReturn>(Func<Expression, Expression, BinaryExpression> joiner, IReadOnlyCollection<Expression<Func<T, TReturn>>> expressions)
{
    if (!expressions.Any())
    {
        throw new ArgumentException("No expressions were provided");
    }
    var firstExpression = expressions.First();
    var otherExpressions = expressions.Skip(1);
    var firstParameter = firstExpression.Parameters.Single();
    var otherExpressionsWithParameterReplaced = otherExpressions.Select(e => ReplaceParameter(e.Body, e.Parameters.Single(), firstParameter));
    var bodies = new[] { firstExpression.Body }.Concat(otherExpressionsWithParameterReplaced);
    var joinedBodies = bodies.Aggregate(joiner);
    return Expression.Lambda<Func<T, TReturn>>(joinedBodies, firstParameter);
}

우리는 이것을 사용할 거예요.Expression.Or과 숫자 표현을 결합하는 것과 같은 다양한 목적으로 같은 방법을 사용할 수 있습니다.Expression.Add.

마지막으로, 이 모든 것을 종합하면, 다음과 같은 것을 얻을 수 있습니다.

var searchCriteria = new List<Expression<Func<Name, bool>>();

  if (!string.IsNullOrWhiteSpace(first))
      searchCriteria.Add(q => q.first.Contains(first));
  if (!string.IsNullOrWhiteSpace(last))
      searchCriteria.Add(q => q.last.Contains(last));
  //.. around 50 additional criteria
var query = Db.Names.AsQueryable();
if(searchCriteria.Any())
{
    var joinedSearchCriteria = Join(Expression.Or, searchCriteria);
    query = query.Where(joinedSearchCriteria);
}
  return query.ToList();

엔티티 프레임워크를 사용하여 동적으로 생성된 쿼리에 "OR" 조건을 추가하는 간단하고 깨끗한 방법이 있습니까?

예, 단 하나의 솔루션에만 의존하면 이를 달성할 수 있습니다.where단일 부울식을 포함하는 절은 다음과 같습니다.OR부품은 런타임에 동적으로 "비활성화"되거나 "활성화"되므로 LINQKit를 설치하거나 사용자 정의 술어 작성기를 작성할 필요가 없습니다.

예를 참조하여 다음을 수행합니다.

var isFirstValid = !string.IsNullOrWhiteSpace(first);
var isLastValid = !string.IsNullOrWhiteSpace(last);

var query = db.Names
  .AsQueryable()
  .Where(name =>
    (isFirstValid && name.first.Contains(first)) ||
    (isLastValid && name.last.Contains(last))
  )
  .ToList();

볼 수 , 는 의예 볼수있듯이서우, 으로 "또는 "전환하고 .where된 전제)를 으로 한 표현(예:isFirstValid).

를 들어, " 를들어약만예약만", "▁for를,들"일 경우isFirstValid아닙니다true,그리고나서name.first.Contains(first)단락되어 실행되지 않거나 결과 세트에 영향을 주지 않습니다.게다가, EF Core의DefaultQuerySqlGenerator내부의 부울식을 더욱 최적화하고 줄입니다.where에 ( 실기전예에하행예(:에전▁before▁executing기e▁(▁it하)false && x || true && y || false && z 간히말하로 줄여도 .y단순한 정적 분석을 통해).

참고:어떤 전제도 다음과 같은 것이 없다면.true그러면 결과 집합이 비어 있게 됩니다. 이 경우에는 원하는 동작일 것입니다. 당신의 그나어이인모에서 모든 요소를 선택하는 것을 합니다.IQueryable출처, 그런 다음 평가하는 식에 최종 변수를 추가할 수 있습니다.true (예:).Where( ... || shouldReturnAll)와 함께var shouldReturnAll = !(isFirstValid || isLastValid)또는 유사한 것).

있는 (더 정확하게는 "중앙화된"한다는 것입니다.where쿼리의 일부).어떤 이유에서인지 술어의 빌드 프로세스를 분산하여 인수로 주입하거나 쿼리 작성기를 통해 연결하려면 다른 답변에서 제안한 술어 작성기를 사용하는 것이 좋습니다. 이 기술을 :) 렇지않으면그기, 한즐기세요을술간이단 :)

Stripling Warrior의 답변을 바탕으로 이 작업을 수행하기 위해 linq 확장자를 작성합니다.

https://github.com/Flithor/ReusableCodes/blob/main/EFCore/OrPredicate.cs

코드(최신 코드가 아닐 수 있음):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Flithors_ReusableCodes
{
    /// <summary>
    /// Make <see cref="IQueryable{T}"/> support or predicate in linq way
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface IQueryOr<T>
    {
        IQueryOr<T> WhereOr(Expression<Func<T, bool>> predicate);
        IQueryable<T> AsQueryable();
    }
    /// <summary>
    /// The extension methods about or predicate
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public static class OrPredicate
    {
        /// <summary>
        /// Private or predicate builder
        /// </summary>
        /// <typeparam name="T"></typeparam>
        private class OrPredicateBuilder<T> : IQueryOr<T>
        {
            List<Expression<Func<T, bool>>> predicates = new List<Expression<Func<T, bool>>>();
            IQueryable<T> sourceQueryable;

            #region private methods
            internal OrPredicateBuilder(IQueryable<T> sourceQueryable) => this.sourceQueryable = sourceQueryable;
            private OrPredicate(IQueryable<T> sourceQueryable, IEnumerable<Expression<Func<T, bool>>> predicates)
            {
                this.sourceQueryable = sourceQueryable;
                this.predicates.AddRange(predicates);
            }

            //===============================================
            // Code From: https://stackoverflow.com/a/50414456/6859121
            private class ExpressionReplacer : ExpressionVisitor
            {
                private readonly Func<Expression, Expression> replacer;

                public ExpressionReplacer(Func<Expression, Expression> replacer)
                {
                    this.replacer = replacer;
                }

                public override Expression Visit(Expression node)
                {
                    return base.Visit(replacer(node));
                }
            }
            private static TExpression ReplaceParameter<TExpression>(TExpression expr, ParameterExpression toReplace, ParameterExpression replacement) where TExpression : Expression
            {
                var replacer = new ExpressionReplacer(e => e == toReplace ? replacement : e);
                return (TExpression)replacer.Visit(expr);
            }
            private static Expression<Func<TEntity, TReturn>> Join<TEntity, TReturn>(Func<Expression, Expression, BinaryExpression> joiner, IReadOnlyCollection<Expression<Func<TEntity, TReturn>>> expressions)
            {
                if (!expressions.Any())
                {
                    throw new ArgumentException("No expressions were provided");
                }
                var firstExpression = expressions.First();
                if (expressions.Count == 1)
                {
                    return firstExpression;
                }
                var otherExpressions = expressions.Skip(1);
                var firstParameter = firstExpression.Parameters.Single();
                var otherExpressionsWithParameterReplaced = otherExpressions.Select(e => ReplaceParameter(e.Body, e.Parameters.Single(), firstParameter));
                var bodies = new[] { firstExpression.Body }.Concat(otherExpressionsWithParameterReplaced);
                var joinedBodies = bodies.Aggregate(joiner);
                return Expression.Lambda<Func<TEntity, TReturn>>(joinedBodies, firstParameter);
            }
            //================================================
            private Expression<Func<T, bool>> GetExpression() => Join(Expression.Or, predicates);
            #endregion

            #region public methods
            public IQueryOr<T> WhereOr(Expression<Func<T, bool>> predicate)
            {
                return new OrPredicate<T>(sourceQueryable, predicates.Append(predicate));
            }
            public IQueryable<T> AsQueryable()
            {
                if (predicates.Count > 0)
                    return sourceQueryable.Where(GetExpression());
                else // If not any predicates exists, returns orignal query
                    return sourceQueryable;
            }
            #endregion
        }

        /// <summary>
        /// Convert <see cref="IQueryable{T}"/> to <see cref="IQueryOr{T}"/> to make next condition append as or predicate.
        /// Call <see cref="IQueryOr{T}.AsQueryable"/> back to <see cref="IQueryable{T}"/> linq.
        /// </summary>
        /// <typeparam name="TSource"></typeparam>
        /// <param name="source"></param>
        /// <returns></returns>
        public static IQueryOr<TSource> AsWhereOr<TSource>(this IQueryable<TSource> source)
        {
            return new OrPredicateBuilder<TSource>(source);
        }
    }
}

사용 방법:

// IQueryable<ClassA> myQuery = ....;
  
var queryOr = myQuery.AsWhereOr();
// for a condition list ...
// queryOr = queryOr.WhereOr(a => /*some condition*/)

myQuery = queryOr.AsQueryable();

맛있게 드세요!

언급URL : https://stackoverflow.com/questions/20054742/dynamic-query-with-or-conditions-in-entity-framework

반응형