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 больше ORM и только для JDBC. А я, в основном, на Android использую.
Условные джоины вполне возможны:
1) Делаете базовый запрос
2) Дополняете джоинами по условию, в зависимости от ваших потребностей
Поясню, что я имею ввиду.
Вот такое как правильно написать?
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 — не такой трудный. Потом не известно что ещё накомпайлит этот Kotlin потом ищи почему перформас упал потому что Kotlin скомпилировал неефективный запрос
Либа ничего не "накомпайлит". Запросы транслируются один в один.
Я бы поспорил с этим. Прежде чем производить какое-то действие, нужно знать, над чем действие будет произведено. Выборка из нескольких таблиц это всего-лишь синтаксический сахар над конструкцией JOIN.
С этим фактом, кстати, связаны множество проблем автокомплита SQL в IDE: невозможно дать подсказку по колонкам таблицы без информации о том, из какой таблицы будет выборка.
Спорно. Это вечная проблема, когда пишешь SELECT
, а автокомплиту нечего тебе предложить, потому что он еще не знает ни таблиц, ни алиасов, ни полей. И когда запросы большие, видишьti.value
, и, мотая вниз, думаешь "это, блин, вообще из какой таблицы???". Чаще всего сначала пишешь select * from
или select 1 from
, потом лепишь "tablesource" при помощи join/apply/where, а потом возвращаешься к списку полей.
В том же linq также пошли.
Ну тогда слово SELECT
становится мусорным и видим то что видим в этой статье или LINQ.
А вообще, черт его знает, что логичнее :). Весь SQL — сборище исторических нелогичных костылей за 50 лет. Точнее продукт эволюции computer science, костылей для обхода текущих возможностей железа, костылей для обхода текущих кривостей реализаии, перетягиваний одеяла между вендорами СУБД и необходимости как-то работать с данными. Дедушка Дейт, вон, тоже постоянно ворчит, что SQL кривым получился.
Пусть он не моден, но… Linq?
join p in products on c.Category equals p.Category into ps
from p in ps.DefaultIfEmpty()
Идея в том, что нет однозначного отображения из LINQ в SQL
x2bool, этот комментарий будет достаточно резкий, но досмотрите его до конца, пожалуйста.
У вас и концепция, и статья, и код получились неудачными. Я отмечу только то, что в глаза бросилось, потому что иначе комментарий будет длиннее статьи.
- В статье, например, синтаксические диаграммы некорректные и бессмысленные.
- Код абсолютно небезопасный и не продуман с точки зрения надёжности: даже прямые включения строк в SQL (привет, injection).
- DDL непонятно когда и непонятно как вызывается. В том смысле, что если есть таблица в БД, то что, её при следующем запуске снова создавать?
- Запросы возможные только совсем-совсем примитивные. Не верите? Берите какой-нибудь http://sql-ex.ru/, нарешайте там штук 20-30 примеров (это несложно) и попробуйте воспроизвести.
- Ваша модель диалектов не позволяет учесть даже базовых различий между СУБД.
- Код на котлине написан "не по-котлински". Совсем не DRY, с кучей явных лишних обработок null. То есть вот просто каждый файл проекта надо брать и почти полностью переписывать.
- Не учтена архитектура предшественников. Тот же linq для начала, ну и ORM типа Hibernate/NHibernate.
- Конечная цель — проверка на этапе компиляции — не достигнута (даже автоинкремент в рантайме проверяется), а где достигнута, то это явным хардкодом типов полей.
На самом деле это всё косметика. Главная проблема — задача просто невообразимо сложнее, чем те приемы, которыми вы её пытаетесь решить. Там прямо в каждой маленькой детали нюансов больше, чем весь проект на текущий момент. С этим подходом не то что до промышленного, до учебного качества проект не довести.
НО.
- Вы сделали прототип. Это важная стадия до которой не добирается и 10% идей, наверное. Этот прототип может быть на выброс, но он компилируется и показывает, куда вы хотите идти.
- Вы правильно сделали, что вынесли прототип на обсуждение. Местами неприятный, но единственный способ получить обратную связь и посмотреть на решение "снаружи". Каждый час, потраченный на то, чтобы идти в неправильную сторону — это в итоге несколько часов потраченных зря.
- Вы правильно заметили, что тулинг между 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'а туда не передашь. Проверяется на этапе компиляции.
Собственно вам нужно:
- Не строку генерить, а PreparedStatement.
- На этапе
.where { e -> e.id eq 1 }
добавлять не "1", а "?". Запоминать индекс, тип и значение. Это довольно не сложно реализовывается, даже дляIN (...)
- На конечном этапе совершить подстановку всех параметров
А можно реализовать запрос такого типа (могу ошибиться с синтаксисом, но хочу донести идею)
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
Функции на подходе. Условные выражения тоже вполне реализуемы.
В Kotlin не получится сделать аналог IQueryable — нужна поддержка Expression в языке. На счет decomposition посмотрите эту ветку: https://habr.com/post/414483/#comment_18792121
Типобезопасный SQL на Kotlin