簡體   English   中英

使用表達式樹循環構建動態查詢

[英]Building dynamic query in a loop using Expression trees

我有一個系統,可以將與Sales相關的不同條件存儲在數據庫中。 加載條件后,它們將用於構建查詢並返回所有適用的Sales。 條件對象如下所示:

ReferenceColumn(他們適用於Sale表中的列)

MinValue(參考列必須為最小值)

MaxValue(參考列必須為最大值)

使用上述條件的集合來搜索“銷售”。 相同類型的ReferenceColumns進行“或”運算,不同類型的ReferenceColumns進行“與”運算。 例如,如果我有三個條件:

ReferenceColumn:“價格”,最小值:“ 10”,最大值:“ 20”

ReferenceColumn:“價格”,最小值:“ 80”,最大值:“ 100”

ReferenceColumn:“年齡”,MinValue:“ 2”,MaxValue:“ 3”

查詢應返回價格在10-20或80-100之間的所有Sales,但前提是這些Sales的年齡在2到3歲之間。

我使用SQL查詢字符串並使用.FromSql來實現它:

public IEnumerable<Sale> GetByCriteria(ICollection<SaleCriteria> criteria)
{
StringBuilder sb = new StringBuilder("SELECT * FROM Sale");

var referenceFields = criteria.GroupBy(c => c.ReferenceColumn);

// Adding this at the start so we can always append " AND..." to each outer iteration
if (referenceFields.Count() > 0)
{
    sb.Append(" WHERE 1 = 1");
}

// AND all iterations here together
foreach (IGrouping<string, SaleCriteria> criteriaGrouping in referenceFields)
{
    // So we can always use " OR..."
    sb.Append(" AND (1 = 0");

    // OR all iterations here together
    foreach (SaleCriteria sc in criteriaGrouping)
    {
        sb.Append($" OR {sc.ReferenceColumn} BETWEEN '{sc.MinValue}' AND '{sc.MaxValue}'");
    }

    sb.Append(")");
}

return _context.Sale.FromSql(sb.ToString();
}

這實際上適用於我們的數據庫,但不適用於其他集合,特別是我們用於UnitTesting的InMemory數據庫,因此我嘗試使用表達式樹來重寫它,我以前從未使用過。 到目前為止,我已經做到了:

public IEnumerable<Sale> GetByCriteria(ICollection<SaleCriteria> criteria)
{
var referenceFields = criteria.GroupBy(c => c.ReferenceColumn);

Expression masterExpression = Expression.Equal(Expression.Constant(1), Expression.Constant(1));
List<ParameterExpression> parameters = new List<ParameterExpression>();

// AND these...
foreach (IGrouping<string, SaleCriteria> criteriaGrouping in referenceFields)
{
    Expression innerExpression = Expression.Equal(Expression.Constant(1), Expression.Constant(0));
    ParameterExpression referenceColumn = Expression.Parameter(typeof(Decimal), criteriaGrouping.Key);
    parameters.Add(referenceColumn);

    // OR these...
    foreach (SaleCriteria sc in criteriaGrouping)
    {
        Expression low = Expression.Constant(Decimal.Parse(sc.MinValue));
        Expression high = Expression.Constant(Decimal.Parse(sc.MaxValue));
        Expression rangeExpression = Expression.GreaterThanOrEqual(referenceColumn, low);
        rangeExpression = Expression.AndAlso(rangeExpression, Expression.LessThanOrEqual(referenceColumn, high));
        innerExpression = Expression.OrElse(masterExpression, rangeExpression);
    }

    masterExpression = Expression.AndAlso(masterExpression, innerExpression);
}

var lamda = Expression.Lambda<Func<Sale, bool>>(masterExpression, parameters);

return _context.Sale.Where(lamda.Compile());
}

當我調用Expression.Lamda時,當前拋出ArgumentException。 Decimal無法在此處使用,它說它想要類型為Sale,但我不知道在其中放置什么內容來進行銷售,而且我不確定這里的位置是否正確。 我還擔心我的masterExpression每次都在復制自己,而不是像對字符串生成器那樣進行追加,但這也許仍然可以工作。

我正在尋求有關如何將此動態查詢轉換為表達式樹的幫助,如果我不在這里,我願意采用一種完全不同的方法。

我想這對你有用

 public class Sale
            {
                public int A { get; set; }

                public int B { get; set; }

                public int C { get; set; }
            }

            //I used a similar condition structure but my guess is you simplified the code to show in example anyway
            public class Condition
            {
                public string ColumnName { get; set; }

                public ConditionType Type { get; set; }

                public object[] Values { get; set; }

                public enum ConditionType
                {
                    Range
                }

                //This method creates the expression for the query
                public static Expression<Func<T, bool>> CreateExpression<T>(IEnumerable<Condition> query)
                {
                    var groups = query.GroupBy(c => c.ColumnName);

                    Expression exp = null;
                    //This is the parametar that will be used in you lambda function
                    var param = Expression.Parameter(typeof(T));

                    foreach (var group in groups)
                    {
                        // I start from a null expression so you don't use the silly 1 = 1 if this is a requirement for some reason you can make the 1 = 1 expression instead of null
                        Expression groupExp = null;

                        foreach (var condition in group)
                        {
                            Expression con;
                            //Just a simple type selector and remember switch is evil so you can do it another way
                            switch (condition.Type)
                            {
//this creates the between NOTE if data types are not the same this can throw exceptions
                                case ConditionType.Range:
                                    con = Expression.AndAlso(
                                        Expression.GreaterThanOrEqual(Expression.Property(param, condition.ColumnName), Expression.Constant(condition.Values[0])),
                                        Expression.LessThanOrEqual(Expression.Property(param, condition.ColumnName), Expression.Constant(condition.Values[1])));
                                    break;
                                default:
                                    con = Expression.Constant(true);
                                    break;
                            }
                            // Builds an or if you need one so you dont use the 1 = 1
                            groupExp = groupExp == null ? con : Expression.OrElse(groupExp, con);
                        }

                        exp = exp == null ? groupExp : Expression.AndAlso(groupExp, exp);
                    }

                    return Expression.Lambda<Func<T, bool>>(exp,param);
                }
            }

            static void Main(string[] args)
            {
                //Simple test data as an IQueriable same as EF or any ORM that supports linq.
                var sales = new[] 
                {
                    new Sale{ A = 1,  B = 2 , C = 1 },
                    new Sale{ A = 4,  B = 2 , C = 1 },
                    new Sale{ A = 8,  B = 4 , C = 1 },
                    new Sale{ A = 16, B = 4 , C = 1 },
                    new Sale{ A = 32, B = 2 , C = 1 },
                    new Sale{ A = 64, B = 2 , C = 1 },
                }.AsQueryable();

                var conditions = new[]
                {
                    new Condition { ColumnName = "A", Type = Condition.ConditionType.Range, Values= new object[]{ 0, 2 } },
                    new Condition { ColumnName = "A", Type = Condition.ConditionType.Range, Values= new object[]{ 5, 60 } },
                    new Condition { ColumnName = "B", Type = Condition.ConditionType.Range, Values= new object[]{ 1, 3 } },
                    new Condition { ColumnName = "C", Type = Condition.ConditionType.Range, Values= new object[]{ 0, 3 } },
                };

                var exp = Condition.CreateExpression<Sale>(conditions);
                //Under no circumstances compile the expression if you do you start using the IEnumerable and they are not converted to SQL but done in memory
                var items = sales.Where(exp).ToArray();

                foreach (var sale in items)
                {
                    Console.WriteLine($"new Sale{{ A = {sale.A},  B =  {sale.B} , C =  {sale.C} }}");
                }

                Console.ReadLine();
            }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM