TrueSql — ультимативный sql-коннектор для Java

от автора

Вступление

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

Java-community

Java-community

Документация, сайт и следующие статьи

Документацию к библиотеке и исходный код вы можете найти тут: https://github.com/pain64/true-sql. В каталоге sample находятся примеры проектов на gradle и maven с использованием TrueSql и Spring Web.

Сайт проекта рекомендуется к посещению всем и особенно заинтересовавшимся: https://truej.net/.

В следующих статьях мы рассмотрим все:

  • Все возможности TrueSql

  • Философия дизайна TrueSql

  • Философия точки G в контексте TrueSql

  • Бенчмарки

  • Почему с TrueSql можно писать в 4 раза меньше кода

  • Экономика выбора TrueSql


ссылка на оригинал статьи https://habr.com/ru/articles/880830/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *