Дерево синтаксиса и альтернатива LINQ при взаимодействии с базами данных SQL

Автор оригинала: Dmitry Tikhonov
  • Перевод


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


История из жизни разработчика


Это был какой-то легаси проект, и меня попросили улучшить его «расширенные» возможности фильтрации.


Раньше у них было что-то вроде этого:


А хотели они что-то такое:



Процедура, которая выполняла «расширенный» поиск выглядела примерно так:


CREATE PROCEDURE dbo.SomeContextUserFind
    (@ContextId int, @Filter nvarchar(max)) AS
BEGIN

DECLARE @sql nvarchar(max) = 
    N'SELECT U.UserId, U.FirstName, U.LastName
    FROM [User] U
    INNER JOIN [SomeContext] [C]
      ON ....
    WHERE [C].ContextId = @p1 AND ' + @Filter;

EXEC sp_executesql 

    @sql,
    N'@p1 int',
    @p1=@ContextId
END

и код, генерировавший строку фильтра, выглядел примерно так:


string BuildFilter(IEnumerable<FilterItem> items)
{
    var builder = new StringBuilder();
    foreach (var item in items)
    {
        builder.Append(item.Field);
        bool isLike = false;
        switch (item.Operation)
        {
            case Operation.Equals:
                builder.Append(" = ");
                break;
            case Operation.Like:
                isLike = true;
                builder.Append(" LIKE ");
                break;
            //...
        }
        builder.Append('\'');
        if (isLike)
            builder.Append('%');
        builder.Append(Escape(item.Value));
        if (isLike)
            builder.Append('%');
        builder.Append('\'');
    }
    return builder.ToString();
}

Конечно, это не лучший код, который вы когда-либо видели, но, к сожалению, легаси проекты (или даже еще не легаси) часто полны таких вещей. В любом случае, что есть, то есть, и это нужно как-то улучшать.


Первая мысль, которая пришла мне в голову, — это просто добавить больше полей в «FilterItem» и усложнить логику построения фильтра, но я быстро понял, что это дорога в никуда ведь поддерживать такой код будет чрезвычайно сложно, и я бы никогда не смог не достичь желаемой функциональности.


В этот момент я вспомнил про «абстрактное синтаксическое дерево», которое, очевидно, является лучшим выбором в данном случае, и далее я объясню, что это такое и как оно помогает.


Абстрактное синтаксическое дерево


Во-первых, давайте проанализируем строки фильтра, которые мы собираемся генерить, например вот эту:


[FirstName]='John' AND ([LastName]='Smith' OR [LastName]='Doe')

И здесь мы можем заметить некоторую структуру:



Эта структура представляет собой дерево, и это означает, что мы можем создать несколько простых классов, которые будут ее описывать:


abstract class Expr
{ }

class ExprColumn : Expr
{
    public string Name;
}

class ExprStr : Expr
{
    public string Value;
}

abstract class ExprBoolean : Expr
{ }

class ExprEqPredicate : ExprBoolean
{
    public ExprColumn Column;
    public ExprStr Value;
}

class ExprAnd : ExprBoolean
{
    public ExprBoolean Left;
    public ExprBoolean Right;
}

class ExprOr : ExprBoolean
{
    public ExprBoolean Left;
    public ExprBoolean Right;
}

Используя классы, мы можем создать объект, который будет представлять исходный фильтр:


var filterExpression = new ExprAnd
{
    Left = new ExprEqPredicate
    {
        Column = new ExprColumn
        {
            Name = "FirstName"
        },
        Value = new ExprStr
        {
            Value = "John"
        }
    },
    Right = new ExprOr
    {
        Left = new ExprEqPredicate
        {
            Column = new ExprColumn
            {
                Name = "LastName"
            },
            Value = new ExprStr
            {
                Value = "Smith"
            }
        },
        Right = new ExprEqPredicate
        {
            Column = new ExprColumn
            {
                Name = "LastName"
            },
            Value = new ExprStr
            {
                Value = "Doe"
            }
        }
    }
};

На самом деле, эта структура, называемая «абстрактным синтаксическим деревом», может использоваться и для представления более сложных запросов, но все они будут иметь один «корень», ссылка на который может храниться в одном объекте.


«Абстрактное синтаксическое дерево» — это результат синтаксического анализа (или парсинга) фразы некоторого формального языка (в нашем случае языка логических выражений) с определенными синтаксическими правилами. Такие правила имеют устоявшийся формат записи. Например, правила нашего простого языка (подмножество логических выражений) могут быть записаны как:


<eqPredicate> ::= <column> = <str>
<or> ::= <eqPredicate>|or|<and> OR <eqPredicate>|or|<and>
<and> ::= <eqPredicate>|(<or>)|<and> AND <eqPredicate>|(<or>)|<and>

Примечание: «Абстрактное» означает, что синтаксис не описывает все детали языка, например, группирующие скобки, лишние пробелы и т. д.


Парсинг — огромная тема сама по себе, и сейчас это не так важно, поскольку у нас уже есть готовые синтаксические деревья, поэтому давайте сосредоточимся на том, что мы можем с ними делать.


Генерация SQL кода


Очевидно, что наиболее важной задачей для нас является преобразование синтаксического дерева обратно в текст, и у нас есть несколько способов сделать это.


Первый способ — использовать сопоставление с образцом (Pattern Matching), что довольно просто:


var filterExpression = ...;

StringBuilder stringBuilder = new StringBuilder();
Match(filterExpression);

void Match(Expr expr)
{
    switch (expr)
    {
        case ExprColumn col:
            stringBuilder.Append('[' + Escape(col.Name, ']') + ']');
            break;
        case ExprStr str:
            stringBuilder.Append('\'' + Escape(str.Value, '\'') + '\'');
            break;
        case ExprEqPredicate predicate:
            Match(predicate.Column);
            stringBuilder.Append('=');
            Match(predicate.Value);
            break;
        case ExprOr or:
            Match(or.Left);
            stringBuilder.Append(" OR ");
            Match(or.Right);
            break;
        case ExprAnd and:
            ParenthesisForOr(and.Left);
            stringBuilder.Append(" AND ");
            ParenthesisForOr(and.Right);
            break;
    }
}

void ParenthesisForOr(ExprBoolean expr)
{
    if (expr is ExprOr)
    {
        stringBuilder.Append('(');
        Match(expr);
        stringBuilder.Append(')');
    }
    else
    {
        Match(expr);
    }
}

В результате в билдере будет следующая строка:


[FirstName]='John' AND ([LastName]='Smith' OR [LastName]='Joe')

Вроде это то, что нам нужно!


"Посетитель"


Хоть мне и нравится функциональное программирование, но в этом случае объектно-ориентированный подход может обеспечить более эффективное решение — я говорю о шаблоне «Посетитель (Visitor)». Идея этого шаблона заключается в том, что мы не пытаемся определить тип объекта, а даем ему список всех возможных действий (в виде интерфейса), и объект сам выбирает действие, наиболее подходящее для его типа. Давайте определим это список:


interface IExprVisitor
{
    void VisitColumn(ExprColumn column);
    void VisitStr(ExprStr str);
    void VisitEqPredicate(ExprEqPredicate eqPredicate);
    void VisitOr(ExprOr or);
    void VisitAnd(ExprAnd and);
}

И любой объект (нашей структуры) может сделать выбор:


abstract class Expr
{
    public abstract void Accept(IExprVisitor visitor);
}

class ExprColumn : Expr
{
    public string Name;

    public override void Accept(IExprVisitor visitor)
        => visitor.VisitColumn(this);
}

class ExprStr : Expr
{
    public string Value;

    public override void Accept(IExprVisitor visitor)
        => visitor.VisitStr(this);
}
...

Теперь мы можем выделить генерацию sql кода в отдельный класс:


class SqlBuilder : IExprVisitor
{
    private readonly StringBuilder _stringBuilder 
        = new StringBuilder();

    public string GetResult()
        => this._stringBuilder.ToString();

    public void VisitColumn(ExprColumn column)
    {
        this._stringBuilder
            .Append('[' + Escape(column.Name, ']') + ']');
    }

    public void VisitStr(ExprStr str)
    {
        this._stringBuilder
            .Append('\'' + Escape(str.Value, '\'') + '\'');
    }

    public void VisitEqPredicate(ExprEqPredicate eqPredicate)
    {
        eqPredicate.Column.Accept(this);
        this._stringBuilder.Append('=');
        eqPredicate.Value.Accept(this);
    }

    public void VisitAnd(ExprAnd and)
    {
        and.Left.Accept(this);
        this._stringBuilder.Append(" AND ");
        and.Right.Accept(this);
    }

    public void VisitOr(ExprOr or)
    {
        ParenthesisForOr(or.Left);
        this._stringBuilder.Append(" OR ");
        ParenthesisForOr(or.Right);
    }

    void ParenthesisForOr(ExprBoolean expr)
    {
        if (expr is ExprOr)
        {
            this._stringBuilder.Append('(');
            expr.Accept(this);
            this._stringBuilder.Append(')');
        }
        else
        {
            expr.Accept(this);
        }
    }

    private static string Escape(string str, char ch)
    {
        ...
    }
}

И использовать его следующим образом:


var filterExpression = BuildFilter();

var sqlBuilder = new SqlBuilder();
filterExpression.Accept(sqlBuilder);
string sql = sqlBuilder.GetResult();

В результате мы получаем желаемую строку:


[FirstName]='John' AND ([LastName]='Smith' OR [LastName]='Joe')

Использование шаблона "Посетитель (Visitor)" имеет несколько преимуществ перед сопоставлением с шаблоном. Так, например, выбор конкретных типов всегда является исчерпывающим, поскольку добавление нового типа в структуру всегда приводит к изменению интерфейса IExprVisitor и, как следствие, необходимости расширения всех его реализаций (в противном случае будут ошибки компиляции).


Обход дерева и круглые скобки


В этом алгоритме есть несколько аспектов, на которые следует обратить внимание.


Во-первых, как это вообще работает?


Фактически, здесь мы выполняем обход синтаксического дерева в глубину, и код sql является следом этого обхода:



Таким образом, каким бы сложным ни было наше дерево, мы всегда получим синтаксически правильную строку.


Во-вторых, что происходит со скобками?


Дело в том, что логические выражения имеют определенный порядок вычисления. Операция «И» вычисляется первой, а операция «ИЛИ» — второй. Скобки необходимы для изменения этой последовательности, поскольку любое выражение в скобках имеет более высокий приоритет при вычислении. Но в синтаксическом дереве последовательность оценки задается самой структурой (от ветвей до корня), поэтому отдельный тип для круглых скобок не требуется.


Расширение синтаксиса


В реальности нам конечно понадобятся и другие предикаты, например, “NotEqual”, и чтобы иметь возможность его (предикат) использовать, нам просто нужно добавить новый класс:


class ExprNotEqPredicate : ExprBoolean
{
    public ExprColumn Column;
    public ExprStr Value;

    public override void Accept(IExprVisitor visitor)
        => visitor.VisitNotEqPredicate(this);
}

А поскольку у нас есть новый тип, то компилятор сообщает нам, что нам нужно реализовать для него генерацию кода SQL:


public void VisitNotEqPredicate(ExprNotEqPredicate exprNotEqPredicate)
{
    exprNotEqPredicate.Column.Accept(this);
    this._stringBuilder.Append("!=");
    exprNotEqPredicate.Value.Accept(this);
}

Итак, мы создали очень простой набор логических предикатов, хотя MS SQL поддерживает гораздо больше, но, как видите, вы можете легко добавить все необходимые языковые структуры.


Кстати, в документации по SQL Server есть все правила синтаксиса SQL, которые весьма полезны для подобного расширения:


Перегрузка операторов


Очевидно, что создание синтаксических деревьев путем вызова конструкторов классов это не самый удобный способ. Однако нам может помочь перегрузка оператора C#. Давайте сделаем следующее:


class ExprColumn : Expr
{
    ...
    public static ExprBoolean operator==(ExprColumn column, string value)
        => new ExprEqPredicate 
        {
            Column = column, Value = new ExprStr {Value = value}
        };

    public static ExprBoolean operator !=(ExprColumn column, string value)
        => new ExprNotEqPredicate
        {
            Column = column, Value = new ExprStr {Value = value}
        };
}

abstract class ExprBoolean : Expr
{
    public static ExprBoolean operator &(ExprBoolean left, ExprBoolean right)
        => new ExprAnd{Left = left, Right = right};

    public static ExprBoolean operator |(ExprBoolean left, ExprBoolean right)
        => new ExprOr { Left = left, Right = right };
}

Теперь мы можем создать дерево синтаксиса очень простым и понятным способом:


ExprColumn firstName = new ExprColumn{Name = "FirstName"};
ExprColumn lastName = new ExprColumn{Name = "LastName"};

var expr = firstName == "John" & (lastName == "Smith" | lastName == "Doe");

var builder = new SqlBuilder();
expr.Accept(builder);
var sql = builder.GetResult();

И результатом будет:


[FirstName]='John' AND ([LastName]='Smith' OR [LastName]='Doe')

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


Что дальше


Кажется, мы решили проблему с фильтрацией, но как насчет сортировки и пейджинга? Также иногда необходимо добавить к оригинальному запросу какой-нибудь под-запрос (View или Derived Table) для фильтрации (или сортировки) по вычисляемым полям.


Не проблема! Давайте просто реализуем весь синтаксис SQL SELECT:


Конечно, вам не нужно делать это самостоятельно, так как есть несколько библиотек (например, моя), где это уже реализовано, поэтому просто рассмотрите этот подход как альтернативный способ взаимодействия с базами данных SQL.


Альтернатива LINQ


То, что мы сделали с перегрузкой операторов, до некоторой степени напоминает выражения LINQ, и на самом деле тут есть некоторые сходства. Компилятор C# генерирует синтаксические деревья, а затем библиотеки, такие как Entity Framework или «LINQ to SQL», преобразуют эти деревья в реальный код SQL.


Основная проблема этого подхода заключается в том, что компилятор генерирует синтаксическое дерево языка C#, а нам то нужен SQL! Отражение императивного C# в декларативный SQL – это не простая задача, а результаты часто непредсказуемы.


Я предпочитаю другой подход — вместо использования синтаксиса C# в качестве основы можно использовать настоящий синтаксис SQL. И вместо компилятора он может использовать перегрузку операторов, методы расширения и различные вспомогательные классы.


При таком подходе, с одной стороны, мы получаем почти такую же гибкость, как если бы мы использовали хранимые процедуры. С другой стороны, у нас строгая типизация, intellisense и бизнес-логика не перемещается в базу данных. И… не нужно каждый раз гуглить, как выполнить LEFT JOIN в LINQ)


Еще одно преимущество заключается в том, что операторы обновления данных (INSERT, UPDATE, DELETE и даже MERGE) также могут быть реализованы таким же образом, и нет необходимости загружать тысячи записей из базы данных для обновления только одного столбца.


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


await InsertInto(tCustomer, tCustomer.UserId)
    .From(
        Select(tUser.UserId)
            .From(tUser)
            .Where(!Exists(
                SelectOne()
                    .From(tSubCustomer)
                    .Where(tSubCustomer.UserId == tUser.UserId))))
    .Exec(database);

Заключение


Синтаксическое дерево — это очень мощная структура данных, с которой вы, вероятно, рано или поздно столкнетесь. Возможно, это будут выражения LINQ, или, может быть, вам надо будет создать анализатор Roslyn, или, может быть, вы самостоятельно захотите создать свой собственный синтаксис, как это я сделал несколько лет назад, чтобы провести рефакторинг одного легаси проекта. В любом случае важно понимать эту структуру и уметь с ней работать.


Link to SqExpress — проект, который частично включает код из этой статьи.

Комментарии 34

    0
    Решал подобную задачу, но я выбрал путь более легкий — вроде это все в рекурсию хорошо ложилось со StringBuilder (что-то отдаленно напоминающий первый вариант ТС).
    Плюс для EF linq — очень хорошая библиотека LINQKit с его PredicateBuilder.
      0

      LINQKit, гвоздями к EF не прибит и это его плюс.

        0
        Кажется, определить Filter(Expression<Predicate> expression) — отдать значение визитору на разбор и не создавать своих классов сильно проще. И использовать можно не создавая объект для каждого Столбец\значение, а работать в синтаксисе c# по свойствам модели. Я примерно так делал генератор Odata запросов.
          0

          Конечная задача это как можно ближе повторить синтаксис SQL, и тут без столбцов уже не обойтись (Для SELECT, ORDER BY, …JOIN и т. д.).

            0
            Да, именно, Вы можете создать любую функцию если не хватит стандартных, и обработать ее нахождение в визитор. Я, скорее, комментом хотел указать на существование expression tree которые Вы повторяете делая подобные классы, только Expression сильно шире, например у них Value может быть как функция и он провалится в ее анализ, что поможет добавить в запросы Distinct или Intersect. Или например в Вашей модели имена не совпадают со столбцами, поможет взять имя из атрибута. Очень широкие возможности анализа кода.
              0

              Если вы про набор типов описанный в статье, то он сильно урезан для упрощения изложения, более-менее реальный будет выглядеть намного сложнее.
              Касательно отдельного типа для столбцов, то он имеет смысл, поскольку в разных диалектах SQL (a SqExpress так же работает и с Postgres SQL) правила записи столбцов будут разными и очень удобно иметь для них отдельный тип, так как кодо-генерацию можно отдельно реализовать для каждого диалекта (условно разная имплементация метода VisitColumnName ).

          0
          объектно-ориентированный подход может обеспечить более эффективное решение — я говорю о шаблоне «Посетитель (Visitor)».

          Вообще говоря, визитор и в ФП прекрасно имеет место, и даже зачастую проще реализуется. А по факту — практически так же.

          И то что вы там еще про pattern matching говорили — тоже в общем не совсем верно, во многих случаях он тоже способе проверять полноту, и тоже ругаться при компиляции, что какие-то случаи не были учтены (при этом будучи более гибким). Ну то есть, я бы сказал, что у ООП в этой ситуации нет каких-то реальных преимуществ при реализации обхода дерева, скорее кому-то удобнее так, а кому-то сяк.
            0

            Возможно, я некорректно противопоставил Visitor и ФП, но конкретно в C# Pattern Matching не проверяет полноту выборки даже в switch expression из C#8 (кроме отсутствия “default arm”, без которого компилятор выдает предупреждение, и Exception в Runtime).


            С подтипами это вполне объяснимо, поскольку есть возможность создать неограниченное число наследников базового класса, но вот с enum ситуация более странная. Казалось бы, в чем проблема понять, что проверяются все возможные значения? Но C# все равно выдает Warning без “default arm”.
            Казалось бы чушь, но в C# enum-у можно присвоить любое числовое значение:


            MyEnum myEnum = (MyEnum)42;

            И тут предупреждение становится понятным.


            Возможно, полнота будет проверяться для Discriminated Unions которые может быть добавят в следующие версии C#, но пока так.

              0
              Ну да, вы пожалуй обобщили ограничения, которые есть в конкретной реализации матчинга на ФП в целом. Но с учетом того что статья про C# — вполне нормально.
                0

                А почему бы не использовать самый прямолинейный способ — интерфейс/абстрактный класс с методом string ToSql(), например?
                Паттерен visitor, обычно, используется когда предполагается большое количество сценариев обхода, в данном случае сценарий один — собрать sql-строку. При этом любому человеку будет понятно, что надо сделать для добавления ещё одного типа узла, взглянув на любой из уже реализованных

                  0

                  Этот паттерн позволяет безгранично добавлять функциональность, никак не меняя исходную структуру. Так, например, в SQ Express c ходу появилось 4 имплементации этого интерфейса:


                  1. Экспорт в T SQL
                  2. Экспорт в Postgres SQL
                  3. Обход AST
                  4. Модификация AST

                  Когда дойдет дело до экспорта в другие диалекты SQL или ещё какие-нибудь хитрые форматы, то будут просто добавлены советующие имплементации.

              0
              Если там синтаксический анализатор, то может лучше flexx, lex, Bison и прочий ANTLR?
                0

                Это инструменты для построения AST путем анализа текста. Здесь же задача обратная — есть AST и надо получить текст.

                +1

                Посмотрите также на linq2db, все те же возможности по удалению, вставке и модификации, merge, только с очень продвинутым LINQ. Жаль что несколько лет назад вы не нагуглили, библиотеке уже лет 8.


                Для примера пепепишу ваш запрос


                var forInsert = 
                   from u in db.GetTable<User>()
                   where !db.GetTable<SubCustomer>().Any(sc => sc.UserId == u.UserId)
                   select u;
                
                await forInsert.Into(db.GetTable<Customer>())
                   .Value(c => c.UserId, u => u.UserId)
                   .InsertAsync();
                  0

                  Да, я потом в итоге её нашел. Хорошая библиотека, но это все-таки LINQ) Мне же хотелось чего-то совсем близкого к оригинальному SQL. К тому же с кастомым синтаксическим деревом можно делать пост-модификации и различные экспорты/импорты, что удобно для передачи по сети или сохранения в базе данных

                    0

                    И это тоже есть. AST можно можно модифицировать или передавать по сети. Если нужен функционал, спросить на github.


                    Хорошая библиотека, но это все-таки LINQ)

                    Вот не вижу минусов, если только вам не надо полную динамику — не знаю какие будут поля, какие будут таблицы.


                    Мне же хотелось чего-то совсем близкого к оригинальному SQL.

                    Так и работает библиотека, только с учетом специфики LINQ. Практически любой SQL я могу выразить с огромной точностью на LINQ без ненужных приседаний с бубнами. Самое главное здесь типобезопасность и возможность компилятора откинуть неправильные запросы еще во время компиляции, также убирается куча бойлерплейта. Под капотом ваши запросы еще и оптимизируются, что дает недюжий выигрыш при композиции запросов.


                    А так у вас получился аналог JOOQ для .NET, а мне жавистов по этому поводу было жаль ;)

                      0
                      Вот не вижу минусов, если только вам не надо полную динамику — не знаю какие будут поля, какие будут таблицы.

                      Ну так задача поставленная в самом начале статьи как бы и подразумевает полную динамику. В реальности это выглядело как: UI контрол создавал AST в JSON который десериализвался в AST булевских выражений который уже вставлялся в некий запрос. Кроме того была проверка на "вычисляемые" столбцы и если они в фильтре были, то добавлялись те или иные вьюхи или Derived Tables. Некоторые предикаты заменялись на выражения вида "EXISTS(SELECT 1 FROM...)". А еще была фильтрация и сортировка по полностью динамическим полям, которые пользователи сами добавляли (как в JIRA)


                      Опять же, это все создавалось не сразу, а по мере развития функциональности и по началу вообще работало в связке с хранимыми процедурами… какой уж там LINQ ))

                        0

                        Возможно я не вижу всей картины, но пока вы рассказывате о том что на linq2db сделать можно. Если там были Table Valued процедуры, они бы в LINQ всторились как родные.

                          0

                          Вполне допускаю, что можно, но свое решение, изначально заточенное под динамику, как-то ближе =) Да и не знали тогда про Linq2Db. Такие статьи (и комментарии к ним) как раз и помогают другим подобрать наиболее адекватное решение типовым, в общем-то, задачам.

                        0

                        Я до этого комментария про JOOQ не слышал. А что с ней не так?

                    +1

                    А этот Legacy проект — он вообще ORM не использовал?

                      0

                      Нет, ORM он не использовал. Изначально все было сделано на хранимых процедурах.

                      +1

                      Комбинация AND и OR в предикатах выборки часто приводит к невозможности использования индексов по участвующим в предикате полям. Можно попробовать разворачивать дерево во вложенный UNION ALL клаус на каждый подуровень — это должно дать шанс движку использовать индексы — он обычно объединения хорошо понимает )

                        0

                        Тут вопрос какому именно движку. Помимо MS SQL, это могут быть Postgres SQL, My SQL и т. д. и как они себя поведут я не знаю. Но вообще идея в том, чтобы дать разработчику максимум контроля над запросом, и если нужен UNION, то будет UNION. Если очень надо, то можно написать кастомный модификатор финального AST.

                        0

                        Ну а разве с помощью LINQKit нельзя сделать выборку из таблицы по полю, переданному в переменной?

                          0

                          Можно :) Но в статье описан подход, который не зависит ни от LINQ, ни от Entity Framework и позволяет писать запросы к базе данных в наиболее близком к оригинальному SQL виде. Недаром я упомянул LEFT JOIN, для реализации которого я всегда лезу в гугл. Дело в том, что LINQ создан в первую очередь для работы с объектами (LINQ to Objects), и с реляционными базами данных в нем возникают типичные для ORM проблемы (тут я могу порекомендовать статью хабре за авторством maslyaev).

                            +1

                            Вообще конечно я перед вами снимаю шляпу! Такую крутую библиотеку написали. Самому давно хотелось написать нечто подобное, но к сожалению, не обладаю такими глубокими познаниями в шарпе. Фильтры на выборку данных приходится писать довольно часто, но я до сих пор не нашел ответа на простой вопрос, как в LINQ сделать выборку из таблицы по полю, которое было передано в переменной? (если вы может где видели, был бы рад ссылочки).


                            На счет LEFT JOIN это действительно магия какая то, тоже постоянно лезу в гугл.


                            Вообще конечно, из личного считаю, считаю LINQ в Entity Framework ужасной штукой. Он как наркотик, подсаживает на быстроту и удобство доставания данных, но когда дело доходит до оптимизаций, или каких то специфических вещей, то тут только понимаешь, что пора с него слезать. Иногда слезать бывает сложно, и дело доходит до подобных вещей. У меня был опыт, когда джун протащил во вьюшку модельку, которую отдал EF, и потом прямо на вьюшке делал циклические подзапросы с помощью lazy loading.


                            Другой раз фильтры организовать, как в начале вашей статьи. Сделать что-то универсальное ну практически не возможно. Только копипаст. Ну ведь хочется, сделать красиво, не писать один и тот же код дважды, но нет, тут так нельзя, нужно копипастить.


                            Из последнего, есть у меня два больших Select(n => ModelA и Select(n => ModelB.
                            ModelA состоит из 50 полей и ModelB состоит из 50 полей и лишь одно поле у них разное. Только одно поле! Но объединить их из под LINQ запрос не как не получится. Тут либо оба поля всегда дергать и не важно, нужно оно там дальше или нет. Либо две разные модели но фактически одного и того же. Если бы я был в мире обычного SQL мне бы удалось достичь подобного парой строк кода, но в мире LINQ over EF, только копипаст. Всерьез уже начинаю задумываться о генераторах кода, которые перед билдом будут создавать нужный код!


                            Это была минутка боли и страданий. Так что как будет возможность, обязательно опробую вашу библиотеку.

                              +1

                              EF Core и его LINQ транслация, это встать и застрелится, кто не согласен, могу поспорить, я посматриваю в их багтрекер.


                              как в LINQ сделать выборку из таблицы по полю, которое было передано в переменной? (если вы может где видели, был бы рад ссылочки).

                              Детский ворос, вам на StackOwerflov ответили бы сразу. Вся магия в лямбдах и визиторах. Решение громоздкое чтобы тут в ответах писать. Но это только по началу.


                              На счет LEFT JOIN это действительно магия какая то, тоже постоянно лезу в гугл.

                              Я вас понимаю, хотя и запомнил ) Так как я один из разработчиков linq2db, то просто придумал новый экстеншин, да и плюю в потолок


                              from t1 in db.Persons
                              from t2 in db.Assignments.LeftJoin(t2 => t2.PresonId == t1.Id)
                              select new {t1, t2}

                              Для EF это аналогично этому


                              from t1 in db.Persons
                              from t2 in db.Assignments.Where(t2 => t2.PresonId == t1.Id).DefaultIfEmpty()
                              select new {t1, t2}

                              Или уж совсем по пуритански (то что вы постоянно гуглите)


                              from t1 in db.Persons
                              join t2 in db.Assignments on t1.Id equals t2.PresonId into j
                              from t2 in j.DefaultIfEmpty()
                              select new {t1, t2}

                              Другой раз фильтры организовать, как в начале вашей статьи. Сделать что-то универсальное ну практически не возможно. Только копипаст.

                              Опять же инструмент нужно знать. Без понимания что такое Expression Tree и как оно организовано — будет только копипаста. Я тут PR подготовил для LINQKit, кучу бойлерплейта можно убрать https://github.com/scottksmith95/LINQKit/pull/127


                              Если бы я был в мире обычного SQL мне бы удалось достичь подобного парой строк кода, но в мире LINQ over EF, только копипаст. Всерьез уже начинаю задумываться о генераторах кода, которые перед билдом будут создавать нужный код!

                              Опять же непонятно что надо. Все можно сделать! Все!

                                0

                                Спасибо огромное за слова поддержки! Это очень важно для мотивации продолжать заниматься Open Source!


                                Касательно выборки по столбцу из переменной — тут действительно, скорее всего, придется руками LINQ выражения править, как советуют выше.


                                По поводу LEFT JOIN, ведь еще есть и FULL JOIN и CROSS APPLY, которые может быть и можно выразить в LINQ, но как сказать об этом query provider (так что бы в SQL появился текст “FULL JOIN”) я не знаю.


                                Вообще “Query Notation” в C# это довольно прикольная штука, которая является аналогом “Do” нотации из Haskell, где она призвана упростить работу с монадами и соответственно в C# c её помощью можно делать разные прикольные штуки с монадо-подобными структурами (вот например моя статья про это "Simplify working with parallel tasks in C#")


                                Но использовать это все для запросов к реляционными базам данных, это, на мой взгляд, попытка натянуть сову на глобус – можно, но получается не очень естественно.

                                  0

                                  Я больше нигде не видел. Но в inq2db можно


                                  from t1 in db.Persons
                                  from t2 in db.Assignments.FullJoin(t2 => t2.PresonId == t1.Id)
                                  select new {t1, t2}

                                  from t1 in db.Persons
                                  from t2 in db.Assignments.RightJoin(t2 => t2.PresonId == t1.Id)
                                  select new {t1, t2}

                                  Ну и CROSS APPLY


                                  from t1 in db.Persons
                                  from t2 in db.FromSql<SomeType>($"SomeFunc({t1.Id})")
                                  select new {t1, t2}
                                  0
                                  Всерьез уже начинаю задумываться о генераторах кода, которые перед билдом будут создавать нужный код!

                                  В отличи от обычного делегата, лямбду на основе Expresison вы можете создать в рантайме, и это никак не замедлит вашу программу. Ну и зачем тут кодогенерация?

                              0

                              del (ошибся веткой)

                                –1
                                Хммм, с таким подходом недалеко и от sql-injection. Я б недопустим такой подход ка базе
                                  +1

                                  Поскольку узлы и листья синтаксического дерева строго типизированы, то отследить все места, где возможна злонамеренная инъекция и принять советующие меры очень легко и это будет на порядок безопаснее чем "text based dynamic sql". В примерах кода статьи я вставил метод Escape, чтобы отразить этот момент.

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое