Как стать автором
Обновить

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

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

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

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

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

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

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

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

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

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

Возможно, я некорректно противопоставил 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#, но пока так.

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

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

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


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

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

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

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

Посмотрите также на 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();

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

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


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

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


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

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


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

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

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


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

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

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

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

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

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

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

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

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

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

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


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


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


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


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


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

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, только копипаст. Всерьез уже начинаю задумываться о генераторах кода, которые перед билдом будут создавать нужный код!

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

Спасибо огромное за слова поддержки! Это очень важно для мотивации продолжать заниматься 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#")


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

Я больше нигде не видел. Но в 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}
Всерьез уже начинаю задумываться о генераторах кода, которые перед билдом будут создавать нужный код!

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

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

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

спасибо класная статья! лучше понял как работают деревья выражений

Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории