Реализовать выражения LINQ to SQL для базы данных с пользовательским форматом даты / времени - PullRequest
3 голосов
/ 18 декабря 2011

Я работаю с базой данных MS-SQL с таблицами, которые используют настроенный формат даты / времени, сохраненный как целое число.Формат поддерживает порядок времени, но не один на один с галочками.Возможны простые преобразования из пользовательского формата в часы / дни / месяцы / и т. Д. - например, я мог бы получить месяц с помощью оператора SQL:

SELECT ((CustomDateInt / 60 / 60 / 24) % 13) AS Month FROM HistoryData

Из этих таблиц мне нужно создавать отчеты,и я хотел бы сделать это с помощью LINQ-to-SQL.Я хотел бы иметь возможность выбирать из множества методов группировки на основе этих дат (по месяцам / по годам и т. Д.).

Я бы предпочел использовать группу команда в LINQ, предназначенная для одного из этих методов группировки.Для повышения производительности я бы хотел, чтобы группировка выполнялась в базе данных, а не перетаскивала все мои данные в объекты POCO, а затем настраивала их после слов.Например:

var results = from row in myHistoryDataContext.HistoryData
              group row by CustomDate.GetMonth(row.CustomDateInt) into grouping
              select new int?[] { grouping.Key , grouping.Count() }

Как реализовать мои функции группировки (например, CustomDate.GetMonth), чтобы они автоматически преобразовывались в команды SQL и выполнялись в базе данных?Нужно ли предоставлять их как Func объекты или объекты Expression <> , или каким-либо другим способом?

Ответы [ 4 ]

4 голосов
/ 20 декабря 2011

Вы не можете написать метод и ожидать, что L2S автоматически узнает, как взять ваш метод и перевести его на SQL.L2S знает о некоторых наиболее распространенных методах, предоставляемых как часть .NET Framework для примитивных типов.Все что угодно, кроме этого, и он не будет знать, как выполнить перевод.

Если вам нужно сохранить свою модель БД как есть:

Вы можете определить методы взаимодействия спользовательский формат и использовать их в запросах.Тем не менее, вам придется помочь L2S с переводом.Для этого вы должны искать вызовы ваших методов в дереве выражений, сгенерированных для вашего запроса, и заменять их реализацией, которую L2S может перевести.Один из способов сделать это - предоставить прокси-реализацию IQueryProvider, которая проверяет дерево выражений для данного запроса и выполняет замену перед передачей его в L2S IQueryProvider для перевода и выполнения.Дерево выражений, которое увидит L2S, можно преобразовать в SQL, поскольку оно содержит только простые арифметические операции, используемые в определениях ваших методов.

Если у вас есть возможность изменить модель БД:

Возможно, вам лучше использовать стандартный тип столбца DateTime для ваших данных.Тогда вы можете смоделировать столбец как System.DateTime и использовать его методы (что понимает L2S).Этого можно достичь, изменив саму таблицу или предоставив представление, которое выполняет преобразование, и обеспечив взаимодействие L2S с представлением.

Обновление : поскольку вам необходимо сохранить текущую модель, вы 'Я хочу перевести ваши методы для L2S.Наша цель - заменить вызовы некоторых конкретных методов в запросе L2S лямбда-выражением, которое L2S может перевести.Все остальные вызовы этих методов, конечно, будут выполняться нормально.Вот пример того, как вы могли бы это сделать ...

static class DateUtils
{
    public static readonly Expression<Func<int, int>> GetMonthExpression = t => (t / 60 / 60 / 24) % 13;
    static readonly Func<int, int> GetMonthFunction;

    static DateUtils()
    {
        GetMonthFunction = GetMonthExpression.Compile();
    }

    public static int GetMonth(int t)
    {
        return GetMonthFunction(t);
    }
}

Здесь у нас есть класс, который определяет лямбда-выражение для получения месяца из целочисленного времени.Чтобы не определять математику дважды, вы можете скомпилировать выражение и затем вызвать его из вашего GetMonth метода, как показано здесь.В качестве альтернативы вы можете взять тело лямбды и скопировать его в тело метода GetMonth.Это пропустит компиляцию выражения во время выполнения и, вероятно, будет выполняться быстрее - в зависимости от того, что вы предпочитаете.

Обратите внимание, что сигнатура GetMonthExpression лямбда-выражения точно соответствует методу GetMonth.Далее мы проверим выражение запроса, используя System.Linq.Expressions.ExpressionVisitor, найдем вызовы к GetMonth и заменим их на нашу лямбду, заменив t значением первого аргумента на GetMonth.

class DateUtilMethodCallExpander : ExpressionVisitor
{
    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        LambdaExpression Substitution = null;

        //check if the method call is one we should replace
        if(node.Method.DeclaringType == typeof(DateUtils))
        {
            switch(node.Method.Name)
            {
                case "GetMonth": Substitution = DateUtils.GetMonthExpression;
            }
        }

        if(Substitution != null)
        {
            //we'd like to replace the method call; we'll need to wire up the method call arguments to the parameters of the lambda
            var Replacement = new LambdaParameterSubstitution(Substitution.Parameters, node.Arguments).Visit(Substitution.Body);
            return Replacement;
        }

        return base.VisitMethodCall(node);
    }
}

class LambdaParameterSubstitution : ExpressionVisitor
{
    ParameterExpression[] Parameters;
    Expression[] Replacements;

    public LambdaParameterExpressionVisitor(ParameterExpression[] parameters, Expression[] replacements)
    {
        Parameters = parameters;
        Replacements = replacements;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        //see if the parameter is one we should replace
        int p = Array.IndexOf(Parameters, node);
        if(p >= 0)
        {
            return Replacements[p];
        }

        return base.VisitParameter(node);
    }
}

Первый класс здесь посетит дерево выражений запросов и найдет ссылки на GetMonth (или любой другой метод, требующий замены) и заменит вызов метода.Замена обеспечивается частично вторым классом, который проверяет заданное лямбда-выражение и заменяет ссылки на его параметры.

Преобразовав выражение запроса, L2S никогда не будет видеть вызовы ваших методов и теперь может выполнятьзапрос, как и ожидалось.

Чтобы перехватить запрос, прежде чем он попадет в L2S удобным способом, вы можете создать своего IQueryable провайдера , который будет использоваться в качестве прокси передL2S.Вы должны выполнить вышеуказанные замены в своей реализации Execute, а затем передать новое выражение запроса поставщику L2S.

2 голосов
/ 27 декабря 2011

Я думаю, что вы можете зарегистрировать свою пользовательскую функцию в DataContext и использовать ее в запросе linq. В этом посте очень хорошо объяснено: http://msdn.microsoft.com/en-us/library/bb399416.aspx

Надеюсь, это поможет.

1 голос
/ 27 декабря 2011

Найдена ссылка на некоторый существующий код, который реализует IQueryable провайдер, как предлагает Майкл.

http://tomasp.net/blog/linq-expand.aspx

Я думаю, что при условии, что код работает, другая давняя проблема заключается в том, что вам необходимо иметь свойство Expression для каждого типа, содержащего дату.

Получившийся код, позволяющий избежать этого, выглядит немного громоздким (хотя он позволит избежать ошибок, которые вы пытаетесь избежать, помещая вычисления в метод):

Выражение группы:

group row by CustomDate.GetMonth(row, x => x.customdate).Compile().Invoke(row)

Метод возврата выражения группы:

public class CustomDate
{
        public static Expression<Func<TEntity, int>> GetMonth<TEntity>(TEntity entity, Func<TEntity, int> func)
        {
            return x => ((func.Invoke(entity)/60/60/24)%13);
        }
}

Я не совсем уверен, вызовет ли этот вложенный .Invoke проблемы с выражением Expandable или нужно будет немного изменить концепцию, но этот код, по-видимому, предоставляет альтернативу созданию собственного IQueryProvider для простых математических выражений.

0 голосов
/ 27 декабря 2011

Кажется, нет никакого способа указать LINQ-to-SQL вызывать ваш SQL UDF. Тем не менее, я считаю, что вы можете инкапсулировать реализацию C # для многократного использования в деревьях System.Linq.Expressions.Expression ...

public class CustomDate {
    public static readonly Expression<Func<int, int>> GetMonth = 
                customDateInt => (customDateInt / 60 / 60 / 24) % 13;
}

var results = from row in myHistoryDataContext.HistoryData
              group row by CustomDate.GetMonth(row.CustomDateInt) into grouping
              select new int?[] { grouping.Key , grouping.Count() }
...