R2DBC Arabba-RELEASE — новый взгляд на реактивное программирование для SQL
Поздравляем Хабр с выходом R2DBC версии Arabba-RELEASE! Это самый первый стабильный релиз проекта.
R2DBC (Reactive Relational Database Connectivity) — открытый проект, посвященный реактивному программированию для SQL. Разработчики R2DBC готовили первую версию спецификации целых два года! В этом хабрапосте мы поговорим, зачем нужен R2DBC, и что происходит в проекте прямо сейчас.
В сообществе Java-разработчиков отношение к реактивщине традиционно крайне неоднозначное. Реактивное программирование даёт значительный прирост к масштабируемости приложения благодаря концепту конвейерной обработки данных. Но это также означает повышенный порог вхождения, а также код, который выглядит совершенно иначе, чем «традиционный» императивный код.
Ключевой «ингредиент» реактивных потоков — неблокирующее исполнение. При этом, используя блокирующие компоненты внутри реактивной системы, можно легко все сломать, ведь в реактивном рантайме используется очень ограниченное количество потоков.
Реактивные приложения, работающие с SQL базами данных, обычно используют JDBC, которое является стандартом для экосистемы JVM. В свою очередь, JDBC дает возможность использовать фреймворки, которые строятся на нем и предоставляют абстракции, позволяющие работать на более высоком уровне и не отвлекаться на технические аспекты общения с базой данных. JDBC — это блокирующий API. Если использовать JDBC в реактивном приложении, то приходится перекладывать блокирующие вызовы на ThreadPool. Как вариант, можно и не использовать JDBC, а напрямую работать с конкретными драйверами баз данных. Но, так или иначе, мы оказываемся перед дилеммой:
- Либо мы используем привычные фреймворки, но упираемся в блокирующие драйвера и работу с ThreadPool
- Либо мы используем неблокирующие драйвера конкретной базы, но теряем возможность использовать JDBC фреймворки
Оба варианта ставят разработчиков в неудобное положение, т.к. ни один из них не решает проблему полностью. Поэтому для создания стандартизированного реактивного API для SQL и был создан R2DBC. Он состоит из спецификации и API. Оба этих компонента описывают, как сделать R2DBC-совместимый драйвер и что разработчики фреймворков могут ожидать от R2DBC в плане функциональности и поведения. R2DBC предоставляет фундамент для подключаемых драйверов.
Зависимости
R2DBC использует Java 8 и требует наличия внешней зависимости на Reactive Streams, потому что в Java 8 нет нативного API для реактивного программирования. Начиная с Java 9, Reactive Streams стала частью самой Java и появился Flow API, поэтому будущие версии R2DBC смогут мигрировать на Flow API сразу же, как только переключатся на Java 9, что позволит R2DBC стать спецификацией без зависимостей на внешние библиотеки.
Структура R2DBC
R2DBC определяет поведение и набор основных интерфейсов, которые используются для интеграции между R2DBC-драйвером и кодом, который к нему обращается. Вот они:
ConnectionFactory
Connection
Statement
Result
Row
Помимо этих интерфейсов, R2DBC идет с комплектом разбитых на категории исключений и интерфейсов метаданных, предоставляющих подробные сведения о драйвере и базе данных.
Главная точка входа в драйвер — ConnectionFactory
. Она создает Connection
, который позволяет общаться с базой данных.
R2DBC использует обычный ServiceLoader
из Java чтобы найти драйверы, лежащие на classpath-е. В приложении, ConnectionFactory
можно получить из URL:
ConnectionFactory connectionFactory = ConnectionFactories
.get("r2dbc:h2:mem:///my-db?DB_CLOSE_DELAY=-1");
public interface ConnectionFactory {
Publisher extends Connection> create();
ConnectionFactoryMetadata getMetadata();
}
Запрос на получение Connection
запускает неблокирующий процесс, соединяющийся с нижележащей базой данных. Сразу после подключения, это соединение используется для контроля транзакционного состояния, или просто чтобы запустить Statement
:
Flux results = Mono.from(connectionFactory.create()).flatMapMany(connection -> {
return connection
.createStatement("CREATE TABLE person (id SERIAL PRIMARY KEY, first_name VARCHAR(255), last_name VARCHAR(255))")
.execute();
});
Давайте посмотрим на интерфейсы Connection
и Statement
:
public interface Connection extends Closeable {
Publisher beginTransaction();
Publisher close();
Publisher commitTransaction();
Batch createBatch();
Publisher createSavepoint(String name);
Statement createStatement(String sql);
boolean isAutoCommit();
ConnectionMetadata getMetadata();
IsolationLevel getTransactionIsolationLevel();
Publisher releaseSavepoint(String name);
Publisher rollbackTransaction();
Publisher rollbackTransactionToSavepoint(String name);
Publisher setAutoCommit(boolean state);
Publisher setTransactionIsolationLevel(IsolationLevel level);
Publisher validate(ValidationDepth depth);
}
public interface Statement {
Statement add();
Statement bind(int index, Object value);
Statement bind(String name, Object value);
Statement bindNull(int index , Class> type);
Statement bindNull(String name, Class> type);
Publisher extends Result> execute();
}
Запустив этот Statement
, на выходе получаем некий Result
. Он содержит информацию либо о количество измененных строк в таблице или сами строки:
Flux results = …;
Flux updateCounts = results.flatMap(Result::getRowsUpdated);
Строки можно обрабаывать потоково. Другими словами, строки появляются сразу же, как драйвер получил и расшифровал эту строку на уровне протокола. Чтобы обработать строки, нужно написать какую-то функцию преобразования, которая будет применяться к каждому расшифрованному Row. Эта функция может извлечь произвольное количество значений и вернуть либо скаляры, либо материализованный объект:
lux results = …;
Flux updateCounts = results.flatMap(result -> result.map((row, rowMetadata) -> row.get(0, Integer.class)));
Посмотрим на интерфейсы Result
и Row
:
public interface Result {
Publisher getRowsUpdated();
Publisher map(BiFunction mappingFunction);
}
public interface Row {
Object get(int index);
T get(int index, Class type);
Object get(String name);
T get(String name, Class type);
}
R2DBC построен на основе Reactive Streams, следовательно, для правильной обработки результатов работы R2DBC стоит использовать реактивную библиотеку. Голый Publisher
практически непригоден для этого. Все примеры кода в этой статье используют Project Reactor.
Облать применения спецификации
R2DBC определяет, как именно должны конвертироваться типы между базой данных и JVM, как конкретные реализации R2DBC должны вести себя. В нем есть гайдлайны по обеспечению совместимости, позволяющие конкретной реализации пройти TCK. R2DBC во многом вдохновлялся JDBC. Именно поэтому, использование R2DBC может показаться вам вполне обычным и привычным. В спецификации затронуты следующие темы:
- Driver SPI and TCK (Technology Compatibility Kit)
- Integration with BLOB and CLOB types
- Plain and Parameterized Statements («Prepared Statements»)
- Batch operations
- Categorized Exceptions (R2dbcRollbackException, R2dbcBadGrammarException)
- ServiceLoader-based Driver Discovery
- Connection URL scheme
Спецификация так же позволяет реализовывать расширения которые могут быть опционально реализованы драйверами в которых есть возможность поддержать эти расширения.
Экосистема
R2DBC начался весной 2018 года как спецификация на драйвер Postgres. Сразу же после изначального ревью, рабочая группа R2DBC осознала, какой вклад R2DBC может сделать в этой области вообще, и поэтому он вырос в целую стандартную спецификацию. Несколько проектов поддержало эти идеи созданием драйверов и библиотек, предназначенных для использования вместе с R2DBC:
Драйверы
- Google Cloud Spanner
- H2
- Microsoft SQL Server
- MySQL
- Postgres
- SAP HANA
Библиотеки
- R2DBC Pool (Connection Pool)
- R2DBC Proxy (Observability Wrapper, похож на P6Spy и DataSource Proxy)
Чтобы сделать свой драйвер R2DBC, в большинстве случаев нужно совершенно по-новому реализовать сетевой протокол, поскольку большинство JDBC-драверов используют внутри SocketInputStream
и SocketOutputStream
. Все такие драйвера — это очень молодые проекты, и использовать их нужно с большой осмотрительностью. Oracle недавно, на конференции Code One, рассказали о планах на драйвер OJDBC 20, сразу после новостей о прекращении работ над ADBA. Оракловский драйвер OJDBC20 будет поставляться с несколькими реактивными расширениями, вдохновленными работой над ADBA и обратной связью от рабочей группы R2DBC, поэтому этот драйвер можно будет использовать в реактивных приложениях.
Несколько поставщиков баз данных также заинтересованы в создании R2DBC-драйверов.
То же самое справедливо и для фреймворков. Проекты вроде R2DBC Client, kotysa и Spring Data R2DBC — все они позволяют использовать R2DBC в приложениях. Другие библиотеки, например, jOOQ, Micronaut и Hibernate Rx — уже в курсе о существовании R2DBC и тоже хотят когда-нибудь проинтегрироваться с ним.
Что почитать, где забрать?
Начать можно вот с чего:
R2DBC представляет из себя release train Arabba-RELEASE
, состоящий из модулей:
Артефакты этого релиза:
При использовании Maven, нужно добавить pom.xml
следуюище строки:
io.r2dbc
r2dbc-bom
Arabba-RELEASE
pom
import
io.r2dbc
r2dbc-postgresql
io.r2dbc
r2dbc-pool
Что дальше?
R2DBC Arabba-RELEASE — это первый стабильный релиз открытого стандарта. Развитие спецификации на этом не останавливается: хранимые процедуры, расширения транзакций, спецификация событий базы данных (таких как Postgres Listen/Notify) — это всего несколько из тем, запланированных на следующую версию, R2DBC 0.9.
Присоединяйтесь к сообществу — это позволит не только следить за разработкой, но и поучаствовать в ней!