Pull to refresh

Comments 56

Как-то чужеродно выглядит range-оператор .. в этом dsl. И eq тоже режет глаз.

Альтернатива range это что-нибудь типа listOf, очень многословно получится. Что касается eq, то переопределить == не получится, т.к. он обязан вернуть Boolean, а для нужд библиотеки подойдет только внутренний тип Expression.

Предпосылки-то понятны, но не уверен, что это самый лучший вариант. Какие варианты, кстати, вообще рассматривались?

Можно в методе прямо прописать, можно строкой прямо. Не уверен, что это чем-то лучше. Есть другие идеи?

Зато можно инфиксную == в backticks определить. В JVM, но не js, правда


    val c = A() `==` A()

    class A{
        infix fun `==` (b: A) : A {
            return A()
        }    
    }

Можно. Но нужно ли? Как по мне, так eq лаконичнее, плюс там все операторы придется так делать. Еще я не уверен, что DEX позволит такое имя, не хотелось бы исключать Android.

В JS можно использовать аннотацию JsName:


class A {
    @JsName("eqeq")
    infix fun `==`(b: A): String = "OK"
}
Ну и почему тогда не упомянули фирменный фреймворк Exposed от самих JetBrains?

Exposed больше ORM и только для JDBC. А я, в основном, на Android использую.

Возможно ли с использованием данного DSL построить запрос, в котором количество join'ов зависит от внешних условий?

Условные джоины вполне возможны:
1) Делаете базовый запрос
2) Дополняете джоинами по условию, в зависимости от ваших потребностей

«Я не настоящий сварщик», с Kotlin не работал.

Поясню, что я имею ввиду.

Вот такое как правильно написать?
x = from(Employees)
    .join(Organizations).on { e, o -> o.id eq e.organizationId }
    .where { e, o -> e.organizationId eq 1 }
    .select { e, o -> e.id .. e.name .. o.name }
if <some external condition>
  x = x.join(Countries).on { e, o, c  -> e.country_id = c.id } # Уже 3 переменные в роли алиасов
end if
if <some other external condition>
  x = x.join(Users).on { e, o, c, u  -> e.user_id = u.id } # Уже 3 либо 4 переменные в роли алиасов 
end if 

Так — нет. Можно, что-то такое:


x = from(Employees)
    .join(Organizations).on { e, o -> o.id eq e.organizationId }

if (some external condition)
  y = x.join(Countries).on { e, o, c  -> e.country_id = c.id } # Уже 3 переменные в роли алиасов
       .where { e, o, c -> e.organizationId eq 1 }
       .select { e, o, c -> e.id .. e.name .. o.name }
else
  y = x.where { e, o -> e.organizationId eq 1 }
       .select { e, o -> e.id .. e.name .. o.name }
Дублирование этого кода — сильно не комильфо.
.where { e, o, c -> e.organizationId eq 1 }
.select { e, o, c -> e.id .. e.name .. o.name }


я так понимаю это ограничение языка?

Нет, это не ограничение языка, так написана библиотека. Select, являясь завершающим вызовом в любом случае будет дублироваться. Над Where можно подумать еще.

Я бы подумал на тему явного объявления алиасов при построении запросов.

У меня примерно такие мысли появились:
# Declare aliases first
val e = generate_alias
val o = generate_alias
val c = generate_alias
val u = generate_alias

# Bind e to Employee
x = from(Employees, e)
# bind o to Organizations
x = x.join(Organizations, o).on { |o| -> o.id eq e.organizationId }

# Pick columns from employees table
x = x.select { e.id .. e.name }
# Add column from organizations table
x = x.select { o.name }

if some_condition
  # Join countries and add countries.country_name to select
  x = x.join(Countries, c).on { |c| e.country_id = c.id }
          .select { c.country_name }

if another_condition
  # Join countries and add users.user_name to select
  x = x.join(Users, u).on { |u| e.user_id = u.id }
          .select { u.user_name }


x.where { e.name eq… }

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

Кстати, как и сами выражения on. Если язык позволяет, я бы их объединил с join.
Вот не вижу смысла, зачем прослойка между девелопером и SQL?
SQL — не такой трудный. Потом не известно что ещё накомпайлит этот Kotlin потом ищи почему перформас упал потому что Kotlin скомпилировал неефективный запрос

Либа ничего не "накомпайлит". Запросы транслируются один в один.

наверное неправильно выразился :) это и хотел сказать — что «накомпайлит»/згенерирует не-ефективный запрос
Видимо снова не так выразились) Запрос же пишет разработчик. Как напишет так и будет, если конечно нет каких то ляпов в библиотеке.
Это как строгая статическая vs слабая динамическая типизация. Проверки во время компиляции радуют. Плюс подозреваю что автокомплит и подсказки на таком коде работать будут, в отличие от SQL.
Отрабатывает если например запрос конкатенацией строк собирается в зависимости от условий? Так то в моей IDE тоже есть, причем очень удобный, конструктор целый.
Эмм… Нет. К сожалению с конкатенацией оно работает плохо. Причём на всех встраиваемых в строки языках. Но если использовать
` .. ${some exp} .. `
из JS, то работает вполне себе сносно.
Первичен все-таки SELECT, а не FROM, т.к. выборки могут происходить из нескольких таблиц сразу, и SELECT в данном случае обозначает намерение и сужает список возможных действий, которые в принципе могут быть выполнены над базой данных.

Я бы поспорил с этим. Прежде чем производить какое-то действие, нужно знать, над чем действие будет произведено. Выборка из нескольких таблиц это всего-лишь синтаксический сахар над конструкцией JOIN.


С этим фактом, кстати, связаны множество проблем автокомплита SQL в IDE: невозможно дать подсказку по колонкам таблицы без информации о том, из какой таблицы будет выборка.

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

Спорно. Это вечная проблема, когда пишешь SELECT, а автокомплиту нечего тебе предложить, потому что он еще не знает ни таблиц, ни алиасов, ни полей. И когда запросы большие, видишьti.value, и, мотая вниз, думаешь "это, блин, вообще из какой таблицы???". Чаще всего сначала пишешь select * from или select 1 from, потом лепишь "tablesource" при помощи join/apply/where, а потом возвращаешься к списку полей.
В том же linq также пошли.

В таком случае, что-то наподобие `SELECT FROM table1, table2 COLUMNS foo, table1.bar AS bar1, table2.bar AS bar2`… было бы логичнее.

Ну тогда слово SELECT становится мусорным и видим то что видим в этой статье или LINQ.
А вообще, черт его знает, что логичнее :). Весь SQL — сборище исторических нелогичных костылей за 50 лет. Точнее продукт эволюции computer science, костылей для обхода текущих возможностей железа, костылей для обхода текущих кривостей реализаии, перетягиваний одеяла между вендорами СУБД и необходимости как-то работать с данными. Дедушка Дейт, вон, тоже постоянно ворчит, что SQL кривым получился.

А как же UPDATE, INSERT, DELETE?..
А что они? У них таблица в начале обозначена. Там, конечно, может для update/delete для некоторых СУБД и алиас быть, но хоть какой-то референс.
Да и очень давно: LinqToDB
То что здесь описано лишь одна сотая часть того что может LinqToDB. Уже и не припомню когда голые SQL писал.
Если вы имеет в виду Linq to SQL то это не совсем полноценный аналог, поскольку в нем не строится в явном виде синтаксическое дерево SQL запросов. Linq пытается преобразовать выражения на языке C# в SQL запрос, что не дает полноценного контроля над результатом.
Можете уточнить что вы имеете ввиду под «полноценным контролем над результатом»?
Допустим мне нужен LEFT JOIN. С билдером синтаксического дерева, я вызываю функцию “LeftJoin”. В случае LINQ я лезу в гугл и нахожу, что именно вот такая последовательность вызовов будет интерпретирована как Left Join:
join p in products on c.Category equals p.Category into ps
from p in ps.DefaultIfEmpty()

Идея в том, что нет однозначного отображения из LINQ в SQL
Рассмотрим, например, FULL JOIN. В библиотеке LinqToDb вижу, что добавили эту функцию, но в оригинальном ответе я ссылался на Linq to SQL где ничего подобного нет.
Идея в том, что нет однозначного отображения из LINQ в SQL

То что MS библиотеки его не сделали, не значит что это невозможно. В LinqToDB, как раз делается все, чтобы вы могли написать практически любой запрос на LINQ. Недавно пал CTE.

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


  1. В статье, например, синтаксические диаграммы некорректные и бессмысленные.
  2. Код абсолютно небезопасный и не продуман с точки зрения надёжности: даже прямые включения строк в SQL (привет, injection).
  3. DDL непонятно когда и непонятно как вызывается. В том смысле, что если есть таблица в БД, то что, её при следующем запуске снова создавать?
  4. Запросы возможные только совсем-совсем примитивные. Не верите? Берите какой-нибудь http://sql-ex.ru/, нарешайте там штук 20-30 примеров (это несложно) и попробуйте воспроизвести.
  5. Ваша модель диалектов не позволяет учесть даже базовых различий между СУБД.
  6. Код на котлине написан "не по-котлински". Совсем не DRY, с кучей явных лишних обработок null. То есть вот просто каждый файл проекта надо брать и почти полностью переписывать.
  7. Не учтена архитектура предшественников. Тот же linq для начала, ну и ORM типа Hibernate/NHibernate.
  8. Конечная цель — проверка на этапе компиляции — не достигнута (даже автоинкремент в рантайме проверяется), а где достигнута, то это явным хардкодом типов полей.

На самом деле это всё косметика. Главная проблема — задача просто невообразимо сложнее, чем те приемы, которыми вы её пытаетесь решить. Там прямо в каждой маленькой детали нюансов больше, чем весь проект на текущий момент. С этим подходом не то что до промышленного, до учебного качества проект не довести.


НО.


  1. Вы сделали прототип. Это важная стадия до которой не добирается и 10% идей, наверное. Этот прототип может быть на выброс, но он компилируется и показывает, куда вы хотите идти.
  2. Вы правильно сделали, что вынесли прототип на обсуждение. Местами неприятный, но единственный способ получить обратную связь и посмотреть на решение "снаружи". Каждый час, потраченный на то, чтобы идти в неправильную сторону — это в итоге несколько часов потраченных зря.
  3. Вы правильно заметили, что тулинг между kotlin и db далёк от совершенства. Эту тему есть куда развивать.

Насколько я понимаю, эта библиотека используется вами в другом проекте (или планировалась для этого). Сделайте паузу в развитии kuery, попробовав его использовать. Если не сможете — не используйте, но запишите, что помешало использовать. Не тратьте времени больше, чем на фикс критичных багов. За 1-2 месяца вы будете гораздо лучше знать, что именно нужно полностью переработать в kuery. Не бросайте, возможно у вас получится то, что задумано изначально, но получится другой полезный и удобный инструмент для разработки.

Спасибо за развернутый комментарий. Приятно, когда вникают в суть статьи. Теперь по пунктам:


1) хм???
2) Абсолютно верно. Это вообще решается плэйсхолдерами и передачей аргументов в prepared statements, т.е. хэндлиться должно уровнем ниже. Думаю стоит этот момент задокуметировать, чтобы не сбивать с толку людей.
3) Никакой магии не происходит "за кадром". Вызывать DDL нужно руками. Библиотека не является средством миграции или ORM. Сейчас цель сделать этакий билдер для SQL запросов.
4) Правда. Как доберусь до подзапросов, будет немного лучше.
5) Очень может быть. Библиотека в продакшене используется только на Android (SQLite). Может, я что-то упустил из других диалектов.
6) Очень может быть
7) Не совсем понял. С LINQ я "на ты", т.к. по основному роду деятельности я дотнетчик.
8) Согласен. Полностью проверить запросы на этапе компиляции не получится, но можно добиться некоторого улучшения по сравнению с SQL. В рантайме автоинкремент не проверяется.


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

Полностью проверить запросы на этапе компиляции не получится

Собственно стандартный JDBC


stmt.setInt(1, someInt)

И ничего, кроме Int'а туда не передашь. Проверяется на этапе компиляции.


Собственно вам нужно:


  1. Не строку генерить, а PreparedStatement.
  2. На этапе .where { e -> e.id eq 1 } добавлять не "1", а "?". Запоминать индекс, тип и значение. Это довольно не сложно реализовывается, даже для IN (...)
  3. На конечном этапе совершить подстановку всех параметров

А это идея! Можно сделать методы расширения для конкретных реализаций типа JDBC для преобразования в prepared statement вместо строк. Благодарю за наводку.

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


INSERT INTO table1 (field1) VALUES (SELECT MAX(field2)*2 FROM TABLE 2 WHERE somefield IN ("Q", "W", "E") GROUP BY someotherfield) 

Это частный случай подзапроса. Подзапросы еще не готовы, но первые в очереди на реализацию. Будет.

А функции, case и прочее? Я, конечно, утрирую, но все же


CASE 
  WHEN (LENGTH(SUBSTR(TRIM(field1), 1, 5)) + INSTR(LOWER(field2), field3) + field4) / 2 < field5: "foo",
  WHEN field6 > random(): "bar"
  ELSE: "baz"
END

Функции на подходе. Условные выражения тоже вполне реализуемы.

Хотел спросить, у вас возможен query decomposition? Одну часть запроса написал и используешь везде. Ведь вы знакомы с IQueryable и что с этим можно вытворять.
А не получается ли что вы пишете новый облегченный JOOQ? Хотя, почему бы Котлину не поддержать Expression Tree, также как async await. Вот и появились бы первые провайдеры баз данных на JavaVM без текущих ограничений. Думаю что они думают об этом, только в каком это состоянии.

Да, JOOQ в чем то похож своим подходом. На Expression Tree даже тикет вроде где-то был, но пока не приоритетная фича для разработчиков Kotlin :(

Sign up to leave a comment.

Articles