Приветствую!
На протяжении нескольких лет моего опыта в качестве разработчика ПО, не раз сталкивался с ситуацией, когда нужно было выполнить не самый простой SQL-запрос по производительности. Несмотря на то, что не всегда здорово большие аналитические запросы генерить и отдавать на фронт, я напишу статью именно на эту тему. Для примера воспользуемся двумя технологиями, чтобы реализовать простой пример: R2DBC и JDBC
Лежат тут.
Для примера возьмём какую-нибудь большую базу данных из открытого источника. Так как я проживаю в России, то на ум пришла база данных ФИАСа (Федеральная информационная адресная система).
Для начала создадим базу данных jooq_test в PostgreSQL.
Перейдём по ссылке ФИАСа и скачаем нужный архив

Благодаря Linux утилитки pgdbf загрузим данные в базу:
Дождёмся когда все данные в базу прогрузятся и перейдем к следующему этапу.
Обратите внимание, что индексы в таблицах мы не создаём. Это сделано намеренно, чтобы усложнить выборку данных. Цель наша вовсе не в том, чтобы написать оптимальный запрос, а именно в технологическом применении извлечения данных.
Чтобы чуть усложнить запрос сделаем JOIN трёх таблиц, при этом данные возьмём из одной (doma):
И так, с запросом мы определились. Теперь можно приступить к написанию кода.
Как уже было описано выше, стек у нас Spring Boot + MVC + PostgreSQL Driver. Собирать проект мы будем с помощью Gradle
Подробно останавливаться на описании зависимостей мы не будем. Об этом много статей.
Опишем файл настроек в ресурсах проекта
Мы не просто так описали проперти подключения к базе конкретно по пути spring.datasource.hikari. У нас в проекта будет несколько источников данных. Один из которых будет Хикари, а второй R2dbc
В нашем примере весь код лежит в одном пакете: ru.jooq.test.jooqtest. Классов будет немного и для наглядного представления создавать их в бизнесовые пакеты мы не будем.
Как и любое приложение Spring Boot'a всё начинается с аннотации @SpringBootApplication
Здесь тоже ничего нового мы для себя не находим. Продолжим…
Настройки подключения к базе данных у нас лежат в классе JooqConfiguration
Как уже ранее было сказано, у нас два dataSource и по этой причине настройки не по умолчанию spring boot'а.
Обращаться к данным мы будем через протокол HTTP, выходная модель которого представлена в ResultDto-классе
Теперь надо реализовать слой взаимодействия с БД для запроса, который мы описывали ранее.
Для нашего примера мы ограничимся условием лишь на количество строк. Этого достаточно для понимания всех процессов описанных в данной статье. Qualifier(«jdbcDSLContext») мы используем для того, чтобы однозначно определить тот bean, который нам нужен.
Метод getBlockingJooq должен вызываться в сервисе SampleService
Непосредственно сам end-point опишем в SampleController
Здесь мы видим простой RestController и метод jooqBlocking с одним лишь параметром запроса: limit. В сервис мы передаём текущую дату и пришедший параметр от клиента. Ресурс запроса состоит из строки /sample/jooq-blocking, а полный запрос выглядит так:
Чтобы наглядно продемонстрировать работоспособность урла, было записано видео и лежит оно тут
О нём много полезной информации можно найти на Хабре. Первоисточник: r2dbc.io
Технологический стек для данного примера: Spring Boot + Web Flux + R2dbc PostgreSQL Driver
Зависимости в build.gradle.kt нужно подкорректировать:
spring-boot-starter-webflux — это стартер spring WebFlux, который является самодостаточным и несёт в себе практически все необходимые зависимости для «реактивного» взаимодействия.
r2dbc-postgresql — r2dbc драйвер для postgreSQL
Подключение к базе данных нужно немного донастроить, так как драйвер теперь другой, да и сам механизм формирования DSLContext иной:
aplication.yml
Метод в Repository на получение данных реализован через Spring Reactor библиотеку:
Flux — это stream который может отправлять от 0 до N элементов в поток. Он идеально подходит для нашего кейса. Flux наследуется от Publisher, который и публикует сообщение в поток. Более подробно можно почитать тут
Метод очень похож на то, что мы видели в Jdbc-подходе, с одним изменением: вместо списка — stream.
Слой сервиса по аналогии c JDBC тоже следует обновить:
Здесь просто вызываем наш метод из репозитория.
Слой контроллера в WebFlux имеет отличительную особенность в сравнении с MVC. Дело в том, что привычный нам Content-Type: application/json не подходит, так как это блокирующий вызов. Вместо этого нужно использовать Content-Type: application/x-ndjson для стриминга индивидуальных элементов, разделённых новой строкой. Он идеально подходит для нашей концепции.
Curl запрос выглядит так:
Видео, которое демонстрирует работу с R2dbc выложено тут
Для начала запустим наши запросы и посмотрим, какая «картина». Будем запускать 1 млн. строк как для jooq-blocking, так и для jooq-reactive


На первом скриншоте изображён Jdbc запуск, на втором R2dbc


На первом скриншоте изображён Jdbc запуск, на втором R2dbc
Jdbc
R2dbc
В данной статье продемонстрированы самые простые примеры применения реактивного подхода к разработке. Кто-то считает такой способ — будущем, кто-то настоящим, а кто-то и вовсе ненужной фичей. Сравнительный анализ показал, что в целом Spring MVC справляется быст��ее с задачей, однако Spring WebFlux + r2dbc использует значительно меньше ресурсов приложения.
На протяжении нескольких лет моего опыта в качестве разработчика ПО, не раз сталкивался с ситуацией, когда нужно было выполнить не самый простой SQL-запрос по производительности. Несмотря на то, что не всегда здорово большие аналитические запросы генерить и отдавать на фронт, я напишу статью именно на эту тему. Для примера воспользуемся двумя технологиями, чтобы реализовать простой пример: R2DBC и JDBC
Входные данные
- Бизнес сильно ограничен в ресурсах и не готов тратить большие деньги на NoSql специалистов
- В команде есть сильный front-end разработчик, который не боится кешировать данные на фронте и их обрабатывать. Умеет эффективно работать с большим списком
- Есть back-end разработчик, который уважает себя, пишет код для не очень мощного железа
- Задача связана с отображением большого массива данных на фронте (будь то геоданные на карте или картотека в интернет-магазине)
Что будем делать?
- Взять большой массив данных из базы. В примере загружена база ФИАС
- Выполнить запрос и отдать клиенту
План работ
- Написать SQL-запрос на выборку данных
- Написать код, который выполнял запрос и после чего отдавать результат клиенту
- Написать код, который бы сформировал запрос на выборку данных и такой запрос, чтобы при первом найденном результате поиска данные бы отдавались клиенту для скорейшей обработки
- Сравнить результат
Технологический стек
- Spring boot 2.5+
- PostgreSQL
- Jooq Framework
- Spring Webflux
- Srping Web MVC
- R2dbc driver для PostgreSQL
- Jdbc driver для PostgreSQL
Исходники
Лежат тут.
Подготовка данных и запрос на выборку данных
Для примера возьмём какую-нибудь большую базу данных из открытого источника. Так как я проживаю в России, то на ум пришла база данных ФИАСа (Федеральная информационная адресная система).
Для начала создадим базу данных jooq_test в PostgreSQL.
CREATE DATABASE jooq_test;
Перейдём по ссылке ФИАСа и скачаем нужный архив

Благодаря Linux утилитки pgdbf загрузим данные в базу:
pgdbf DOMA.DBF | iconv -c -f cp866 -t UTF-8| psql jooq_test pgdbf FLAT.DBF | iconv -c -f cp866 -t UTF-8| psql jooq_test pgdbf STREET.DBF | iconv -c -f cp866 -t UTF-8| psql jooq_test
Дождёмся когда все данные в базу прогрузятся и перейдем к следующему этапу.
Обратите внимание, что индексы в таблицах мы не создаём. Это сделано намеренно, чтобы усложнить выборку данных. Цель наша вовсе не в том, чтобы написать оптимальный запрос, а именно в технологическом применении извлечения данных.
Чтобы чуть усложнить запрос сделаем JOIN трёх таблиц, при этом данные возьмём из одной (doma):
select d.* from doma d join street s on d.ocatd = s.ocatd join flat f on f.gninmb = s.gninmb
И так, с запросом мы определились. Теперь можно приступить к написанию кода.
Jdbc-подход
Как уже было описано выше, стек у нас Spring Boot + MVC + PostgreSQL Driver. Собирать проект мы будем с помощью Gradle
build.gradle.kts
import nu.studer.gradle.jooq.JooqEdition import org.jetbrains.kotlin.gradle.tasks.KotlinCompile // Необходимы для работы плагины plugins { id("org.springframework.boot") version "2.5.6" id("io.spring.dependency-management") version "1.0.11.RELEASE" id("nu.studer.jooq") version ("6.0.1") kotlin("jvm") version "1.5.31" kotlin("plugin.spring") version "1.5.31" } group = "ru.jooq.test" version = "0.0.1-SNAPSHOT" java.sourceCompatibility = JavaVersion.VERSION_11 repositories { mavenCentral() } val postgresVersion = "42.3.1" dependencies { // Зависимость для jooq jooqGenerator("org.postgresql:postgresql:$postgresVersion") // Spring стартеры implementation("org.springframework.boot:spring-boot-starter-jooq") implementation("org.springframework.boot:spring-boot-starter-jdbc") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") runtimeOnly("org.postgresql:postgresql") testImplementation("org.springframework.boot:spring-boot-starter-test") } tasks.withType<KotlinCompile> { kotlinOptions { freeCompilerArgs = listOf("-Xjsr305=strict") jvmTarget = "11" } } tasks.withType<Test> { useJUnitPlatform() } // генерация классов для Jooq Framework jooq { edition.set(JooqEdition.OSS) configurations { create("main") { jooqConfiguration.apply { jdbc.apply { driver = "org.postgresql.Driver" url = "jdbc:postgresql://localhost:5432/jooq_test" user = "postgres" password = "postgres" } generator.apply { name = "org.jooq.codegen.DefaultGenerator" generate.apply { isDeprecated = false isRecords = true isImmutablePojos = false isFluentSetters = false isJavaBeansGettersAndSetters = false } database.apply { name = "org.jooq.meta.postgres.PostgresDatabase" inputSchema = "public" } target.apply { packageName = "ru.jooq.test.jooqtest.domain" } strategy.name = "org.jooq.codegen.DefaultGeneratorStrategy" } } } } }
Подробно останавливаться на описании зависимостей мы не будем. Об этом много статей.
Опишем файл настроек в ресурсах проекта
application.yml
spring: datasource: driverClassName: org.postgresql.Driver hikari: jdbc-url: jdbc:postgresql://localhost:5432/jooq_test username: postgres password: postgres jooq: sql-dialect: postgres
Мы не просто так описали проперти подключения к базе конкретно по пути spring.datasource.hikari. У нас в проекта будет несколько источников данных. Один из которых будет Хикари, а второй R2dbc
В нашем примере весь код лежит в одном пакете: ru.jooq.test.jooqtest. Классов будет немного и для наглядного представления создавать их в бизнесовые пакеты мы не будем.
Как и любое приложение Spring Boot'a всё начинается с аннотации @SpringBootApplication
JooqTestApplication.kt
package ru.jooq.test.jooqtest import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class JooqTestApplication fun main(args: Array<String>) { runApplication<JooqTestApplication>(*args) }
Здесь тоже ничего нового мы для себя не находим. Продолжим…
Настройки подключения к базе данных у нас лежат в классе JooqConfiguration
JooqConfiguration.kt
package ru.jooq.test.jooqtest import javax.sql.DataSource import org.jooq.DSLContext import org.jooq.impl.DSL import org.jooq.impl.DataSourceConnectionProvider import org.jooq.impl.DefaultConfiguration import org.jooq.impl.DefaultDSLContext import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.jdbc.DataSourceBuilder import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy @Configuration class JooqConfiguration { // конфигурируем dslConext для запросов к СУБД @Bean(value = ["jdbcDSLContext"]) fun jdbcDSLContext(): DSLContext { return DefaultDSLContext(configuration()) } // DataSource из настроек application.yml @Bean @ConfigurationProperties(prefix = "spring.datasource.hikari") fun dataSource(): DataSource { return DataSourceBuilder.create().build() } // Открываем подключение @Bean fun lazyConnectionDataSource(): LazyConnectionDataSourceProxy { return LazyConnectionDataSourceProxy(dataSource()) } @Bean fun connectionProvider(): DataSourceConnectionProvider { return DataSourceConnectionProvider(lazyConnectionDataSource()) } @Bean fun configuration(): DefaultConfiguration { val jooqConfiguration = DefaultConfiguration() jooqConfiguration.set(connectionProvider()) return jooqConfiguration } }
Как уже ранее было сказано, у нас два dataSource и по этой причине настройки не по умолчанию spring boot'а.
Обращаться к данным мы будем через протокол HTTP, выходная модель которого представлена в ResultDto-классе
ResultDto.kt
class ResultDto( val queryTime: LocalDateTime, // Время запроса val dataTime: LocalDateTime, // Время маппинга данных timeToResult: Long? = 0, val dataDto: DataDto // Набор данных из таблицы ) { val timeToResult: Long = Duration.between(queryTime, dataTime).toMillis() } data class DataDto( val name: String, val korp: String, val socr: String, val code: String, val index: String, val gninmb: String, val uno: String, val ocatd: String )
Теперь надо реализовать слой взаимодействия с БД для запроса, который мы описывали ранее.
SampleRepository
package ru.jooq.test.jooqtest import java.time.LocalDateTime import org.jooq.DSLContext import org.jooq.Record8 import org.jooq.SelectLimitPercentStep import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Repository import ru.jooq.test.jooqtest.domain.Tables @Repository class SampleRepository( @Qualifier("jdbcDSLContext") private val jdbcDSLContext: DSLContext ) { private val d = Tables.DOMA private val s = Tables.STREET private val f = Tables.FLAT // Маппинг данных на модель ResultDto fun getBlockingJooq(queryTime: LocalDateTime, limit: Long): List<ResultDto> { return getQuery(jdbcDSLContext, limit) .map { r -> ResultDto( queryTime = queryTime, dataTime = LocalDateTime.now(), dataDto = r.into(d).into(DataDto::class.java) ) } } // SQL-запрос private fun getQuery(dslContext: DSLContext, limit: Long): SelectLimitPercentStep<Record8<String, String, String, String, String, String, String, String>> { return dslContext .select(d.NAME, d.KORP, d.SOCR, d.CODE, d.INDEX, d.GNINMB, d.UNO, d.OCATD) .from(d) .join(s).on(s.OCATD.eq(d.OCATD)) .join(f).on(f.GNINMB.eq(s.GNINMB)) .limit(limit) } }
Для нашего примера мы ограничимся условием лишь на количество строк. Этого достаточно для понимания всех процессов описанных в данной статье. Qualifier(«jdbcDSLContext») мы используем для того, чтобы однозначно определить тот bean, который нам нужен.
Метод getBlockingJooq должен вызываться в сервисе SampleService
SampleService.kt
package ru.jooq.test.jooqtest import java.time.LocalDateTime import org.springframework.stereotype.Service @Service class SampleService(private val sampleRepository: SampleRepository) { // метод, который вызывает репозиторий с SQL-запросом fun getBlockingJooq(queryTime: LocalDateTime, limit: Long): List<ResultDto> { return sampleRepository.getBlockingJooq(queryTime, limit) } }
Непосредственно сам end-point опишем в SampleController
SampleController.kt
package ru.jooq.test.jooqtest import java.time.LocalDateTime import org.springframework.http.MediaType import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController class SampleController(private val sampleService: SampleService) { @GetMapping("/sample/jooq-blocking") fun jooqBlocking(@RequestParam limit: Long): List<ResultDto> { return sampleService.getBlockingJooq(LocalDateTime.now(), limit) } }
Здесь мы видим простой RestController и метод jooqBlocking с одним лишь параметром запроса: limit. В сервис мы передаём текущую дату и пришедший параметр от клиента. Ресурс запроса состоит из строки /sample/jooq-blocking, а полный запрос выглядит так:
curl http://localhost:8080/sample/jooq-blocking?limit=1000000
Чтобы наглядно продемонстрировать работоспособность урла, было записано видео и лежит оно тут
R2dbc-подход
The Reactive Relational Database Connectivity (R2DBC) принёс реактивное API для реляционных баз данных
О нём много полезной информации можно найти на Хабре. Первоисточник: r2dbc.io
Технологический стек для данного примера: Spring Boot + Web Flux + R2dbc PostgreSQL Driver
Зависимости в build.gradle.kt нужно подкорректировать:
dependencies { ... implementation("org.springframework.boot:spring-boot-starter-webflux") runtimeOnly("io.r2dbc:r2dbc-postgresql") }
spring-boot-starter-webflux — это стартер spring WebFlux, который является самодостаточным и несёт в себе практически все необходимые зависимости для «реактивного» взаимодействия.
r2dbc-postgresql — r2dbc драйвер для postgreSQL
Подключение к базе данных нужно немного донастроить, так как драйвер теперь другой, да и сам механизм формирования DSLContext иной:
aplication.yml
spring: r2dbc: url: r2dbc:postgresql://localhost:5432/jooq_test password: postgres username: postgres pool: initial-size: 3 max-size: 10 max-idle-time: 30m jooq: sql-dialect: postgres
@Configuration class JooqConfiguration( private val connectionFactory: ConnectionFactory ) { @Bean(value = ["r2dbcDSLContext"]) fun createContext(): DSLContext { return DSL.using(connectionFactory) } ... }
Метод в Repository на получение данных реализован через Spring Reactor библиотеку:
@Repository class SampleRepository( @Qualifier("r2dbcDSLContext") private val r2dbcDSLContext: DSLContext, @Qualifier("jdbcDSLContext") private val jdbcDSLContext: DSLContext ) { ... fun getReactiveJooq(queryTime: LocalDateTime, limit: Long): Flux<ResultDto> { return Flux.from(getQuery(r2dbcDSLContext, limit)) .map { r -> ResultDto( queryTime = queryTime, dataTime = LocalDateTime.now(), dataDto = r.into(d).into(DataDto::class.java) ) } } ... }
Flux — это stream который может отправлять от 0 до N элементов в поток. Он идеально подходит для нашего кейса. Flux наследуется от Publisher, который и публикует сообщение в поток. Более подробно можно почитать тут
Метод очень похож на то, что мы видели в Jdbc-подходе, с одним изменением: вместо списка — stream.
Слой сервиса по аналогии c JDBC тоже следует обновить:
@Service class SampleService(private val sampleRepository: SampleRepository) { fun getReactiveJooq(queryTime: LocalDateTime, limit: Long): Flux<ResultDto> { return sampleRepository.getReactiveJooq(queryTime, limit) } ... }
Здесь просто вызываем наш метод из репозитория.
Слой контроллера в WebFlux имеет отличительную особенность в сравнении с MVC. Дело в том, что привычный нам Content-Type: application/json не подходит, так как это блокирующий вызов. Вместо этого нужно использовать Content-Type: application/x-ndjson для стриминга индивидуальных элементов, разделённых новой строкой. Он идеально подходит для нашей концепции.
@RestController class SampleController(private val sampleService: SampleService) { @GetMapping("/sample/jooq-reactive", produces = [MediaType.APPLICATION_NDJSON_VALUE]) fun jooqReactive(@RequestParam limit: Long): Flux<ResultDto> { return sampleService.getReactiveJooq(LocalDateTime.now(), limit) } ... }
Curl запрос выглядит так:
curl http://localhost:8080/sample/jooq-reactive?limit=1000000
Видео, которое демонстрирует работу с R2dbc выложено тут
Сравнительный анализ
Для начала запустим наши запросы и посмотрим, какая «картина». Будем запускать 1 млн. строк как для jooq-blocking, так и для jooq-reactive
Потребление памяти


На первом скриншоте изображён Jdbc запуск, на втором R2dbc
Загрузка CPU


На первом скриншоте изображён Jdbc запуск, на втором R2dbc
Время выполнения
Jdbc
GET http://localhost:8080/sample/jooq-blocking?limit=1000000 HTTP/1.1 200 OK Content-Type: application/json Content-Length: 279170577 > 2021-11-18T181435.200.json Response code: 200 (OK); Time: 25541ms; Content length: 273258295 bytes
R2dbc
GET http://localhost:8080/sample/jooq-reactive?limit=1000000 HTTP/1.1 200 OK transfer-encoding: chunked Content-Type: application/x-ndjson > 2021-11-18T180754.200.txt Response code: 200 (OK); Time: 47203ms; Content length: 279975556 bytes
Заключение
В данной статье продемонстрированы самые простые примеры применения реактивного подхода к разработке. Кто-то считает такой способ — будущем, кто-то настоящим, а кто-то и вовсе ненужной фичей. Сравнительный анализ показал, что в целом Spring MVC справляется быст��ее с задачей, однако Spring WebFlux + r2dbc использует значительно меньше ресурсов приложения.