Нативный клиент на Java
Клиентская библиотека для работы с YDB из JVM была написана вскоре после появления самой СУБД Яндекса. Этот нативный клиент использовался внутри компании: он позволял сервисам подключаться к кластерам YDB по собственному API поверх gRPC и делать запросы на языке YQL, строго типизированном диалекте SQL.
Нативный клиент необходим, потому что без него программистам пришлось бы каждый раз писать сотни строк типового кода для работы с gRPC-протоколом, соединениями, контролем ошибок, сценариями использования YDB.
Такой клиент был разработан с нуля, он скрывает от программистов gRPC-протокол и учитывает специфику распределённой СУБД: хранение топологии кластера, установку приоритетов для узлов, выбор, к каким узлам устанавливать подключения для тех или иных запросов. Это было низкоуровневое решение.
Пример кода ниже демонстрирует использование нативного клиента для выполнения запроса к базе данных. Можно увидеть типизированный YQL вместо привычного SQL, явное указание типов для аргументов запроса, выполнение в retry-контексте, использование сессии, настройку уровня изоляции транзакции, асинхронный код, работу с объектом — результатом запроса и другие низкоуровневые штуки:
String query
= "DECLARE $seriesId AS Uint64; "
+ "DECLARE $seasonId AS Uint64; "
+ "SELECT sa.title AS season_title, sr.title AS series_title "
+ "FROM seasons AS sa INNER JOIN series AS sr ON sa.series_id = sr.series_id "
+ "WHERE sa.series_id = $seriesId AND sa.season_id = $seasonId";
// Type of parameter values should be exactly the same as in DECLARE statements.
Params params = Params.of(
"$seriesId", PrimitiveValue.newUint64(seriesID),
"$seasonId", PrimitiveValue.newUint64(seasonID)
);
QueryReader result = retryCtx.supplyResult(
session -> QueryReader.readFrom(session.createQuery(query, TxMode.SNAPSHOT_RO, params))
).join().getValue();
ResultSetReader rs = result.getResultSet(0);
while (rs.next()) {
logger.info("read season with title {} for series {}",
rs.getColumn("season_title").getText(),
rs.getColumn("series_title").getText()
);
}

JDBC-драйвер
Нативный клиент можно использовать внутри компании, но в опенсорсе хорошим тоном считается предлагать разработчикам в каждом стеке то, с чем они привыкли иметь дело. Для Java-разработчиков наиболее привычный интерфейс — это JDBC: стандарт взаимодействия Java-приложений с разными СУБД.
Мы реализовали JDBC поверх нативного клиента, переиспользовав функциональность подключения к кластеру и работы с пулом сессий. Это распространённый способ реализации JDBC-драйверов для сложных систем.
Интерфейс JDBC использует абстракцию java.sql.Connection
. Ожидается, что эта абстракция будет инкапсулировать TCP-подключение к базе данных, поэтому код third-party-библиотек обычно оперирует «пулами» таких подключений с помощью javax.sql.DataSource
. Для поддержки внешних пулов JDBC-адаптер YDB использует виртуальные JDBC-подключения, которые синхронизируются с физическими gRPC-подключениями в нативном клиенте.
YDB под капотом использует собственный строго типизированный диалект SQL, который называется YQL. Чтобы prepared statements с ординальными параметрами через символы ? работали корректно, JDBC-адаптер разбирает эти выражения на клиенте и преобразует их в YQL. Вот SQL-запрос, переданный JDBC-адаптеру:
SELECT sa.title AS season_title, sr.title AS series_title
FROM seasons AS sa INNER JOIN series AS sr ON sa.series_id = sr.series_id
WHERE sa.series_id = ? AND sa.season_id = ?;
Он преобразуется в запрос к YDB, как в примере ниже. Тип YDB определяется по типу переданного Java-объекта, либо его можно указать явно:
DECLARE $seriesId AS Uint64;
DECLARE $seasonId AS Uint64;
SELECT sa.title AS season_title, sr.title AS series_title
FROM seasons AS sa INNER JOIN series AS sr ON sa.series_id = sr.series_id
WHERE sa.series_id = $seriesId AND sa.season_id = $seasonId;
JDBC-адаптер для YDB позволяет выполнять все SQL-запросы, которые можно сделать с помощью нативного клиента. Можно пользоваться DataGrip, DBeaver. И главное, нет необходимости в специфическом для нативного клиента коде (его вы видели в начале этой статьи). JDBC-адаптер превращает retry context в стандартный SQLRecoverableException
, с которым может работать third-party-код.

Миграции: Liquibase и Flyway
Имплементация стандарта JDBC служит хорошим фундаментом для поддержки популярных в мире Java-решений. Например, Liquibase: популярной утилиты для безопасного управления миграциями.
В 2024 году я уже писал на Хабре про расширение YDB для Liquibase и для Flyway. Благодаря этим доработкам расширение YDB можно использовать как зависимость в Spring Boot. А утилиты Flyway и Liquibase — в standalone-режиме с помощью .jar-файлов диалекта.

ORM: стандарт JPA и его имплементация Hibernate
Следующий шаг для поддержки YDB в экосистеме Java — написание приложений, использующих популярный стандарт JPA в реализации Hibernate. Эта реализация генерирует SQL-код из Java-кода и применяет «диалекты» для поддержки разных СУБД.
Диалекты Hibernate не зависят напрямую от JDBC-драйвера, так как отвечают только за генерацию валидного SQL-кода и сами никак не взаимодействуют с СУБД. Hibernate выбирает диалект по URL к базе данных, и можно явно указать нужный диалект, например YDB.
spring.jpa.properties.hibernate.dialect=tech.ydb.hibernate.dialect.YdbDialect
Новые диалекты могут наследоваться от существующего или же от org.hibernate.dialect.Dialect
, как это сделано для диалекта YDB. Код этого диалекта реализован по-разному для Hibernate 5 и Hibernate 6. Возможно, в будущих версиях мы сделаем сделать отдельные реализации для следующих версий Hibernate, если сломается какая-либо обратная совместимость, как это произошло между 5-й и 6-й версиями.
Hibernate использует парсер ANTLR, чтобы распарсить запросы на собственном языке HQL и преобразовать их в AST-дерево. Затем Hibernate вызывает функции диалекта и передаёт ему это дерево, чтобы диалект мог «обойти» его и модифицировать SQL-запрос, который формирует Hibernate.
Диалект YDB для Hibernate выполняет несколько модификаций в формируемом запросе. Например, для синтаксически сложных имён вместо двойных кавычек используются бэктики, а в булевом контексте 1 и 0 заменяются true и false.
Диалект регистрирует новые типы, например Datetime
, чтобы программисты могли использовать эти типы в своих классах и были уверены, с каким типом в базе данных будут сопоставлены поля. Также диалект генерирует схемы базы данных и отвечает за сопоставление типов. А бонусом к разработке диалекта YDB для Hibernate стало то, что появилась поддержка Spring Data JPA «из коробки».

Ещё один ORM: Spring Data JDBC
Spring Data генерирует SQL-запросы по сигнатурам методов. Так же, как в случае с Hibernate, диалект вызывается для узлов AST. Но абстрактное синтаксическое дерево в этом случае гораздо проще из-за меньшей функциональности библиотеки. А с помощью аннотации Query можно писать сразу YQL и использовать любые запросы:
interface SimpleUserRepository : ListCrudRepository<User, Long> {
fun findByUsername(username: String): User?
}
SELECT `Users`.`id` AS `id`, `Users`.`username` AS `username`,
`Users`.`lastname` AS `lastname`, `Users`.`firstname` AS `firstname`
FROM `Users` WHERE `Users`.`username` = ?
Для поддержки YDB в Spring Data достаточно добавить получившийся диалект в проект. При этом нет никаких синтаксических ограничений: SQL (а точнее, YQL) всегда корректно генерируется по сигнатуре метода.

SQL-билдер jOOQ
Если Hibernate и Spring Data JDBC генерируют запросы к базе данных на основании Java-кода, то jOOQ, наоборот, генерирует Java-классы из схемы базы данных. И уже эти сгенерированные классы используются как высокоуровневый ORM для формулирования запросов.
Интеграция YDB для jOOQ включает генератор, собственную стратегию генерации для более читаемых пакетов и расширяет DSL-контекст такими конструкциями YDB, как UPSERT INTO и REPLACE INTO.
ydbDSLContext.upsertInto(SERIES)
.set(record)
.execute()
upsert into `episodes`
(`series_id`, `season_id`, `episode_id`, `title`, `air_date`)
values (?, ?, ?, ?, ?)
Главной сложностью в при создании генератора стало то, что проект jOOQ не предназначен для того, чтобы другие компании писали код для поддержки своих СУБД. Авторы jOOQ ожидают, что они сами будут вести разработку для спонсоров проекта. Поэтому для имплементации пришлось использовать package-private-классы билдеров, что корректно работает с последней на момент публикации версией jOOQ 3.19.0, но может сломаться в будущих версиях.
Интеграция позволяет писать YQL-специфичные билдеры и даёт полный контроль над генерацией SQL с помощью Java-классов. Для Spring Boot при этом нужно использовать свой starter, так как по умолчанию порождается DSL context.

Разработка интеграций делает базу данных лучше
Поддержка популярных инструментов — большой вызов для новой базы данных и её языка запросов. Интеграции с опенсорс-решениями, о которых рассказано в этой статье, позволяют базе данных эволюционировать в соответствии с ожиданиями внешних клиентов и стандартов языков, предлагать привычные инструменты.
База данных YDB доступна как опенсорс‑проект и как коммерческая сборка с открытым ядром. Вы можете запустить её на своих серверах или воспользоваться нашим managed‑решением в Yandex Cloud.
Мы общаемся с нашими пользователями в телеграме и на Хабре. Пишите комментарии к этой статье: мне, как разработчику базы данных, будет интересно поговорить с теми, кто базами данных пользуется!