Вступление
Несколько лет назад мы осознали, что острая проблема Java это ее классические библиотеки для backend-разработки и решили что-то с этим сделать. На типовом проекте ~50% кода это работа с базой данных. В процессе решения задачи мы опирались на определенные метрики, которые (спойлер) на самом деле и определяют возможные решения. Метрики брались не с потолка и мотивированы тремя точками зрения:
так как мы сами пишем много софта, мы смотрели на технологию с позиции разработчика, который устал от существующих решений
с точки зрения бизнеса, стоимость решения задачи на TrueSql должна быть дешевле в несколько раз, а может и на порядок
как люди с высоким чувством прекрасного, для нас был важен высокий технический уровень реализации и совместимости
Метрики:
размер реализации технологии -> min
размер полной документации -> min
количество абстракций, с которыми будет взаимодействовать программист -> min
оверхед библиотеки по производительности -> min
удобство отладки -> max
качество, устойчивость, предсказуемость -> max
безопасность -> max
количество кода в приложении с TrueSql -> min
проверки на этапе компиляции -> max
устранение мапперов, лишних уровней “архитектуры”, генерация выходных DTO
Мы начали решать задачу Java-библиотеки для работы с базой данных с создания micro-ORM, однако в процессе оказалось что никакая ORM библиотека не сможет удовлетворить наши метрики. Также мы публиковали статью о проблемах самой популярной на платформе Java библиотеки для работы с БД. Поэтому наше решение это НЕ ORM (определение в статье) и НЕ QueryBuilder. Нам было очень приятно, что многие поддерживают нашу позицию. И мы сделали максимум чтобы TrueSql был ультимативным решением. Встречайте TrueSql – the ultimate database connector.
TrueSql - основные возможности
ResultSet to DTO mapping. Grouped object-tree fetching
Compile-time query validation
DTO generation
Null-safety
-
Full featured:
Generated keys
Update count
Batching
Transactions and connection pinning
Streaming fetching
Stored procedure call
Unfold parameters for "in-clause"
Multiple database schemas in module
Extra type bindings
DB constraint violation checks
100% sql-injection safety guarantee
Exceptional performance. Equal to JDBC
Можно не поверить, но весь этот функционал укладывается в документацию на 12 минут чтения, но сейчас мы покажем конфигурацию и фетчинг.
Базовый API
Рассмотрим Hello, word на TrueSql
// ! ANNOTATE YOUR CLASS WITH @TrueSql !
@TrueSql class Main {
// 1. declare DataSourceW or ConnectionW as connection configuration
@Configuration(
checks = @CompileTimeChecks(
url = "jdbc:postgresql://localhost:5432/test_db",
username = "user",
password = "userpassword"
)
) static class PgDs extends DataSourceW {
public PgDs(DataSource w) { super(w); }
}
void main() {
// 2. open connection pool
var ds = new PgDs(new HikariDataSource() {{
setJdbcUrl("jdbc:postgresql://localhost:5432/test_db");
setUsername("user");
setPassword("userpassword");
}});
// 3. do querying
var name = ds.q("select name from users where id = ?", 42)
.fetchOne(String.class);
}
}
Данный пример (один файл) является самодостаточным. Важно, что вся конфигурация осуществляется в Java коде. Наконец-то мы можем писать Java-программы на Java! Теперь вам не нужно искать в подвалах интернета какие же ключи подкинуть в yml файл – все скажет IDE.
Конфигурация состоит из двух частей: этапа компиляции (через аннотации) и этапа исполнения через override методов DataSourceW.
На шаге 1 в аннотации @Configuration мы задаем параметры подключения к базе для проверки запросов на этапе компиляции (для CI/CD их можно будет переопределить через env).
На шаге 3 мы делаем простой запрос к базе. Аннотация @TrueSql включает для этого файла процессор аннотаций, который и проверяет запросы. Все, больше никаких аннотаций в API нет.
Рассмотрим простые примеры.
Вставка новых строк:
var id = ds.q(
"insert into owners values(default, ?, ?, ?, ?, ?)",
firstName, lastName, address, city, telephone
).asGeneratedKeys("id").fetchOne(int.class);
На этапе компиляции будет проверено:
Корректность запроса (полная, сервером СУБД)
Правильность типов переданных аргументов
Выходная колонка id (generated keys) действительно присутствует
Тип результата равен типу колонки id
Tree-select:
Основная проблема интерфейса работы с реляционной базой данных заключается в том, что результат это всегда некоторая таблица. По этой причине требуется дальнейшая предобработка для выдачи этих данных на frontend. В итоге классический jdbc-way не прижился на типовых Java бэкэндах. TrueSql решает эту проблему:
record Pet(int id, String name) {}
record Owner(
int id, String firstName, String lastName,
List<Pet> pets
) {}
var owner = ds.q("""
select
o.id, o.first_name, o.last_name,
p.id, p.name
from owners o
left join pets p on o.id = p.owner_id
where o.id = ?""", id
).fetchOneOrZero(Owner.class);
Мы сразу загрузили Owner’а со списком ассоциированных с ним Pet’ов. Данная Dto уже готова к отправке на frontend. По DTO TrueSql автоматически определяет поля для группировки и агрегации (на любое количество уровней). Если делать это руками, то пришлось бы написать примерно подобный код (P.S часть кода, которую сгенерировал TrueSql):
Код маппинга
var mapped = Stream.iterate(
rs, t -> {
try {
return t.next();
} catch (SQLException e) {
throw source.mapException(e);
}
}, t -> t
).map(t -> {
try {
return
new Row (
new net.truej.sql.bindings.IntegerReadWrite().get(rs,1),
new net.truej.sql.bindings.StringReadWrite().get(rs,2),
new net.truej.sql.bindings.StringReadWrite().get(rs,3),
new net.truej.sql.bindings.IntegerReadWrite().get(rs,4),
new net.truej.sql.bindings.StringReadWrite().get(rs,5)
);
} catch (SQLException e) {
throw source.mapException(e);
}
})
.collect(
java.util.stream.Collectors.groupingBy(
r -> new G1(
r.c1,
r.c2,
r.c3
), java.util.LinkedHashMap::new, Collectors.toList()
)
).entrySet().stream()
.filter(g1 ->
java.util.Objects.nonNull(g1.getKey().c1) ||
java.util.Objects.nonNull(g1.getKey().c2) ||
java.util.Objects.nonNull(g1.getKey().c3)
).map(g1 ->
new com.example.demo.api.OwnersApi.Owner3(
EvenSoNullPointerException.check(g1.getKey().c1),
g1.getKey().c2,
g1.getKey().c3,
g1.getValue().stream().filter(r ->
java.util.Objects.nonNull(r.c4) ||
java.util.Objects.nonNull(r.c5)
).map(r ->
new com.example.demo.api.OwnersApi.Pet3(
EvenSoNullPointerException.check(r.c4),
r.c5
)
).distinct().toList()
)
);
Заметим, что TrueSql генерирует оптимальный эквивалентный jdbc-код, а значит TrueSql имеет лучший runtime-performance по сравнению с другими библиотеками.
Tree-select и точка G режим:
И даже этого нам было мало! Наша задача окончательно закрыть вопрос с boilerplate!
import demo.api.OwnersApiG.*;
var owner = ds.q("""
select
o.id,
o.first_name ,
o.last_name ,
p.id as "Pet pets.",
p.name as " pets."
from owners o
left join pets p on o.id = p.owner_id
where o.id = ?""", id
).g.fetchOneOrZero(Owner.class);
TrueSql может сам сгенерировать DTO (создать классы Owner и Pet) исходя из тела любого запроса. ДАЖЕ ЕСЛИ ВАМ НУЖНЫ ГРУППИРОВКИ!!! Группы размечаются в алиасах к именам колонок (as):
p.id as "Pet pets.",
p.name as " pets."
Сгенерированные DTO
public static class Pet {
@NotNull public final int id;
@Nullable public final java.lang.String name;
public Pet(
int id,
java.lang.String name
) {
this.id = id;
this.name = name;
}
@Override public boolean equals(Object other) {
return this == other || (
other instanceof Pet o &&
java.util.Objects.equals(this.id, o.id) &&
java.util.Objects.equals(this.name, o.name)
);
}
@Override public int hashCode() {
int h = 1;
h = h * 59 + java.util.Objects.hashCode(this.id);
h = h * 59 + java.util.Objects.hashCode(this.name);
return h;
}
}
public static class Owner {
@NotNull public final int id;
@Nullable public final java.lang.String firstName;
@Nullable public final java.lang.String lastName;
public final List<Pet> pets;
public Owner(
int id,
java.lang.String firstName,
java.lang.String lastName,
List<Pet> pets
) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.pets = pets;
}
@Override public boolean equals(Object other) {
return this == other || (
other instanceof Owner o &&
java.util.Objects.equals(this.id, o.id) &&
java.util.Objects.equals(this.firstName, o.firstName) &&
java.util.Objects.equals(this.lastName, o.lastName) &&
java.util.Objects.equals(this.pets, o.pets)
);
}
@Override public int hashCode() {
int h = 1;
h = h * 59 + java.util.Objects.hashCode(this.id);
h = h * 59 + java.util.Objects.hashCode(this.firstName);
h = h * 59 + java.util.Objects.hashCode(this.lastName);
h = h * 59 + java.util.Objects.hashCode(this.pets);
return h;
}
}
Это невероятно круто. И вопрос стоит гораздо глубже чем избавление от “лишнего кода”, но об этом потом.
Композиция
var discounts = List.of(
new DateDiscount(
LocalDate.of(2024, 7, 1),
new BigDecimal("0.2")
),
new DateDiscount(
LocalDate.of(2024, 8, 1),
new BigDecimal("0.15")
)
);
ds.q(
discounts, """
update bill b
set discount = amount * ?
where cast(b.date as date) = ?""",
v -> new Object[]{v.discount, v.date}
)
.asGeneratedKeys("id", "discount")
.withUpdateCount
.g.fetchList(Discount.class);
var expected = new UpdateResult<>(
new long[]{2L, 2L},
List.of(
new Discount(1L, new BigDecimal("20.00")),
new Discount(2L, new BigDecimal("20.00")),
new Discount(3L, new BigDecimal("15.00")),
new Discount(4L, new BigDecimal("15.00"))
)
);
batch update + generated keys + update count + .g !!!
Api TrueSql имеет АДСКИ-ХОРОШИЕ возможности композиции. За это была уплочена высокая цена — месяц ежедневного трехразового употребления пуэра.
Хотелось бы рассказать и про силу modifying CTE / unfold, и про простоту работы с транзакциями и про многие другие ультимативности, но об этом в рамках других статей.
Нам говорили: это невозможно в Java
Как это работает? А все просто – Java 5 annotaion processors дают нам возможность генерировать классы (dto и jdbc-код делающий fetch). Спецификация JDBC 1.0 от января 1997 года позволяет получать метаданные запроса не исполняя его (PreparedStatement.{getMetadata(), getParametersMetadata()}). Современные версии баз данных хорошо поддерживают этот API. Ну и конечно TrueSql работает потому что мы вложили большую часть себя и МНОГО ДУМАЛИ чтобы обеспечить композицию и сделать все правильно.

Поддержка баз данных
TrueSql поддерживает любую базу данных у которой есть jdbc-драйвер. Для этих драйверов в TrueSql дополнительно реализован слой совместимости, исправляющий несоответствие спецификации: PostgreSQL, MySQL, MSSQL, Oracle, MariaDB, HSQL.
Покрытие тестами
TrueSql имеет 100% test coverage (bytecode), весь API имеет тесты.
Размер реализации
Размер реализации с тестами занимает 10k LOC против, например, 1.3M LOC Hibernate Core. Чтение всей документации занимает 12 минут.
Выигрыш в количестве кода
Кодовая база типового проекта (spring-pet-clinic), написанного на TrueSql, меньше в 4 раза по сравнению с типичными кодовыми базами на Hibernate / Spring Data JPA!
Лицензия
Наша цель это TrueSql на каждом Java-проекте. Мы могли бы пойти по коммерческой модели JOOQ, но тогда все бы продолжили страдать. Единственный способ получить market share и забыть про все это ORM-безумие на Java платформе – дать современное решение по бесплатной лицензии. Поэтому TrueSql распространяется под лицензией Apache 2.0.
Что это значит для меня?
Ночь прошла настало утро. TrueSql подходит для любого проекта с перечисленными СУБД. Неважно кто вы - разработчик, CTO или владелец IT-компании - теперь у вас есть TrueSql. Уже с сегодняшнего дня вы можете писать выразительный, быстрый, недорогой и качественный код.

Документация, сайт и следующие статьи
Документацию к библиотеке и исходный код вы можете найти тут: https://github.com/pain64/true-sql. В каталоге sample находятся примеры проектов на gradle и maven с использованием TrueSql и Spring Web.
Сайт проекта рекомендуется к посещению всем и особенно заинтересовавшимся: https://truej.net/.
В следующих статьях мы рассмотрим все:
Все возможности TrueSql
Философия дизайна TrueSql
Философия точки G в контексте TrueSql
Бенчмарки
Почему с TrueSql можно писать в 4 раза меньше кода
Экономика выбора TrueSql
Комментарии (9)
panzerfaust
10.02.2025 09:55Не очень понимаю восторги. У нас в 2 проектах точно такие же штуки написаны. В одном попроще, в другом чуть посложнее. Аннотаций, кстати, нет ни там ни там. Где попроще - там самая увесистая часть это маппер из кортежей в объекты на 370 строк.
У нас запросы к БД простецкие, для них такой колхоз пойдет. Как общее решение для вообще любого проекта - да ну, вы шутите. При живом-то JOOQ.
PastorGL
10.02.2025 09:55Оно редко за 2~3к строк вылазит, если только генератора триггеров или ещё какой-нибудь эзотерической фигни не требуется.
А на современной жабе с рекордами писать такое вообще считай читерство. Не нужно, как во времена 6, извращаться с рефлексией.
Andrey_Solomatin
10.02.2025 09:55Для большей трушности можно еще добавить github workflow для релиза, и для солидности добавить тесты на других версиях джава https://github.com/pain64/true-sql/blob/main/.github/workflows/gradle.yml#L28.
goodfup Автор
10.02.2025 09:55Спасибо за предложение, и за то что ознакомились с проектом. Пока пароли от maven central храним на отдельной машине. По поводу версий Java, то основная версия это 21+. Во многом, потому что было самим очень удобно реализовать TrueSql на свежей джаве. Бэкпорты на 17 и ниже стоят в планах, но тут ждем запроса от вендоров
johndow
10.02.2025 09:55Я просто оставлю это здесь: https://jdbi.org/
goodfup Автор
10.02.2025 09:55Приветствую!
Во время проектирования TrueSql, jdbi тоже изучался. Из принципиальных вещей там нет:
Проверок запросов на этапе компиляции
Генерации выходных dto
Древовидных выборок, которые полностью меняют ситуацию с отчетами и rest-контроллерами
И много чего там еще нет или сделано неправильно
DenSigma
10.02.2025 09:55Как описывается маппинг от таблицы/запроса к полям dto (бизнес-объекта)? С условием, что dto нельзя засирать аннотациями? Имеется отдельный слой persistence, в методы которого входят/выходят dto.
goodfup Автор
10.02.2025 09:55Приветствую!
record User(long id, String name) {}
ds.q("select id, name from users").fetchList(User.class)
Шаг 1. TrueSql, в классе dto (User) ожидает конструктор с ненулевым количество параметров. Если его нет, или их несколько - ошибка компиляции
Шаг 2. Генерирует маппер вида
new User(rs.getLong(1), rs.getString(2))
Шаг 3. Если включена проверка запросов, то проверяется соответствие выходных колонок запроса сигнатуре конструктора (количество параметров, типы)
А в режиме генерации dto:
ds.q("select id, name from users").g.fetchList(User.class)
TrueSql сам создаст класс User. Имена полей получит путем преобразования имен выходных колонок из snake_case в camelCase
PastorGL
Хмм... Любой уважающий себя Java-разработчик обязан написать как минимум 1 ORM/JDBC-коннектор, 1 контейнер/DI/AOP, 1 кодогенератор/интерпретатор/компилятор. А лучше 5. Это единственный нормальный путь становления сеньором — и других наверное, нет.
Если вам удалось в одном проекте покрыть сразу несколько перечисленных категорий, значит, вы всё делаете правильно, и станете хорошим Java-сеньором. А если у вас хватает наглости выложить его в паблик, и добавить в название true/ultimate, то вы имеете все шансы стать просто отличным Java-сеньором. Большинство почему-то стесняется :)
Но хватит похвалы. Подобного DO layers написано очень много — я лично штук 6 имплементаций видел, заточенных под конкретный проект, и сам как минимум три писал (ещё более специализированных). У всех один недостаток: если надо сделать шаг влево или вправо от видения автора, то сразу приплыли. Сколько ни думай головой, заранее всех кейсов не продумаешь.
Но за выкладку в паблик зачёт, конечно.