Перейти до основного вмісту

DatabaseClient в WebFlux

DatabaseClient у Spring WebFlux

DatabaseClient — це низькорівневий реактивний API для роботи з реляційними базами даних у Spring WebFlux. Він входить до складу Spring Data R2DBC та дозволяє виконувати SQL-запити без використання блокуючого JDBC.

Якщо JdbcTemplate є стандартним рішенням для Spring MVC, то DatabaseClient виконує аналогічну роль у реактивному стеку. Він надає простий спосіб виконання SQL-запитів, прив'язки параметрів та перетворення результатів у Java-об'єкти.

DatabaseClient працює поверх драйвера R2DBC і повертає реактивні типи Mono<T> та Flux<T>, що дозволяє повністю зберігати неблокуючу модель виконання.

DatabaseClient добре підходить у таких випадках:

  • необхідно виконувати власні SQL-запити;
  • потрібен повний контроль над SQL;
  • проєкт використовує Spring WebFlux та R2DBC;
  • запити занадто складні для похідних методів Repository;
  • необхідно отримувати лише окремі поля, а не цілі сутності;
  • потрібно виконувати JOIN, агрегатні функції або складні фільтрації.

Фактично DatabaseClient займає проміжне місце між реактивним Repository та повністю ручною роботою з драйвером R2DBC.

Додавання залежностей

Для Maven необхідно підключити Spring Data R2DBC та драйвер потрібної бази даних.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-r2dbc</artifactId>
    </dependency>

    <dependency>
        <groupId>io.r2dbc</groupId>
        <artifactId>r2dbc-postgresql</artifactId>
    </dependency>
</dependencies>

Налаштування підключення

У більшості випадків достатньо стандартного налаштування в application.yml.

spring:
  r2dbc:
    url: r2dbc:postgresql://localhost:5432/demo
    username: postgres
    password: password

Spring Boot автоматично створить ConnectionFactory та DatabaseClient.

Ін'єкція DatabaseClient

У сервісі достатньо отримати DatabaseClient через конструктор.

@Service
public class UserService {

    private final DatabaseClient databaseClient;

    public UserService(DatabaseClient databaseClient) {
        this.databaseClient = databaseClient;
    }
}

Отримання одного запису

public Mono<User> findById(Long id) {
    return databaseClient
            .sql("""
                    SELECT id, name, email
                    FROM users
                    WHERE id = :id
                    """)
            .bind("id", id)
            .map((row, metadata) -> new User(
                    row.get("id", Long.class),
                    row.get("name", String.class),
                    row.get("email", String.class)
            ))
            .one();
}

Метод one() повертає Mono<User>. Якщо запис не знайдено, буде повернуто порожній Mono.

Отримання списку записів

public Flux<User> findAll() {
    return databaseClient
            .sql("""
                    SELECT id, name, email
                    FROM users
                    ORDER BY name
                    """)
            .map((row, metadata) -> new User(
                    row.get("id", Long.class),
                    row.get("name", String.class),
                    row.get("email", String.class)
            ))
            .all();
}

Метод all() повертає Flux<User>.

Передача параметрів

DatabaseClient підтримує іменовані параметри.

databaseClient
    .sql("""
            SELECT *
            FROM users
            WHERE age > :age
              AND status = :status
            """)
    .bind("age", 18)
    .bind("status", "ACTIVE");

Такий підхід робить SQL більш читабельним і захищає від SQL Injection.

INSERT-запит

public Mono<Integer> save(User user) {
    return databaseClient
            .sql("""
                    INSERT INTO users(name, email)
                    VALUES (:name, :email)
                    """)
            .bind("name", user.getName())
            .bind("email", user.getEmail())
            .fetch()
            .rowsUpdated();
}

Метод rowsUpdated() повертає кількість змінених рядків.

UPDATE-запит

public Mono<Integer> updateEmail(Long id, String email) {
    return databaseClient
            .sql("""
                    UPDATE users
                    SET email = :email
                    WHERE id = :id
                    """)
            .bind("email", email)
            .bind("id", id)
            .fetch()
            .rowsUpdated();
}

DELETE-запит

public Mono<Integer> delete(Long id) {
    return databaseClient
            .sql("""
                    DELETE
                    FROM users
                    WHERE id = :id
                    """)
            .bind("id", id)
            .fetch()
            .rowsUpdated();
}

Переваги DatabaseClient

  • повністю реактивний API;
  • не блокує потоки;
  • працює без Hibernate;
  • простий у використанні;
  • повний контроль над SQL;
  • легко оптимізувати продуктивність запитів;
  • добре підходить для складних JOIN та агрегатних запитів;
  • менше прихованої логіки порівняно з ORM.

Недоліки

  • не підтримує автоматичне формування SQL, як JPA;
  • не має кешування першого рівня;
  • немає механізму Lazy Loading;
  • необхідно самостійно описувати SQL;
  • потрібно вручну виконувати мапінг рядків у Java-об'єкти;
  • при великій кількості запитів код може стати доволі об'ємним.

DatabaseClient vs R2dbcRepository

Обидва підходи є частиною Spring Data R2DBC, але вирішують різні задачі.

R2dbcRepository DatabaseClient
Автоматична реалізація CRUD Ручне написання SQL
Менше коду Більше контролю
Прості CRUD-операції Складні SQL-запити
Менше гнучкості Максимальна гнучкість
Похідні методи пошуку Будь-який SQL
Менше можливостей оптимізації Легше оптимізувати продуктивність

У невеликих CRUD-проєктах зазвичай достатньо R2dbcRepository. Він дозволяє швидко реалізувати базові операції без написання SQL.

Якщо ж застосунок активно працює з великими обсягами даних, використовує складні JOIN, агрегації, CTE, віконні функції або потребує максимальної продуктивності, DatabaseClient стає значно кращим вибором. Він надає повний контроль над SQL і дозволяє використовувати всі можливості конкретної СУБД.

На практиці ці підходи часто поєднують: для звичайних CRUD-операцій використовують R2dbcRepository, а для складних або оптимізованих запитів — DatabaseClient. Це дозволяє отримати баланс між швидкістю розробки та продуктивністю застосунку.

Коментарі

Популярні публікації

Колекції в Elixir

Списки Що таке список у Elixir У Elixir список — це впорядкована колекція елементів, реалізована як однозв’язний список . Це означає, що кожен елемент (вузол) зберігає посилання на наступний, але не на попередній. Така структура дозволяє швидко додавати елементи на початок списку, але повільно доступати до довільного елементу. У Java подібну структуру представляє LinkedList — частина Java Collections Framework. Вона реалізована як двозв’язний список, що забезпечує зручне додавання/видалення елементів з початку або кінця списку. Створення списків У Elixir список створюється за допомогою квадратних дужок: list = [1, 2, 3, 4] Додавання елементів У Elixir новий елемент можна додати тільки на початок списку за допомогою оператора | : # Elixir list = [2, 3, 4] new_list = [1 | list] # [1, 2, 3, 4] Доступ до елементів У Elixir немає прямого доступу до елементів за індексом, але це можна зробити через Enum.at : Enum.at([10, 20, 30], 1) # 20 Ітерація по списку У Elixir ...

Інструменти для роботи з Node.js

Що таке npm? npm (Node Package Manager) — це офіційний пакетний менеджер для Node.js . Він дозволяє: Встановлювати сторонні бібліотеки та фреймворки Керувати залежностями проєкту Запускати скрипти (команди) через package.json Приклад ініціалізації проєкту з npm npm init -y Файл package.json (скорочений приклад) { "name": "my-project", "version": "1.0.0", "scripts": { "start": "ts-node src/index.ts", "build": "tsc" }, "dependencies": { "express": "^4.18.0" }, "devDependencies": { "typescript": "^5.0.0", "ts-node": "^10.0.0" } } Що таке tsconfig.json? tsconfig.json — це файл конфігурації для компілятора TypeScript, який визначає, як слід компілювати код. Приклад файлу tsconfig.json { "compilerOptions": { "target": "ES2020...

Атоми в мові програмування Elixir

Атоми в Elixir Атоми є фундаментальною концепцією в Elixir , що відіграє ключову роль у створенні надійних та масштабованих систем. В Elixir це специфічний тип даних, який є константою , незмінною , ідентифікованою за своїм ім'ям . Отже, атом в Elixir — це іменована константа, що представляє себе. Уявіть, що ви даєте унікальне ім'я певній речі, і це ім'я завжди посилається саме на цю річ, і ніколи на щось інше. Наприклад, атом :ok завжди буде означати саме успішне завершення операції, а не якесь інше значення. Технічно, атоми є похідними від чисел . Кожен унікальний атом зберігається у таблиці атомів, і йому присвоюється унікальний цілочисельний ідентифікатор. Це робить їх надзвичайно ефективними для порівняння: замість порівняння рядків (що є повільною операцією), Elixir порівнює цілочисельні ідентифікатори. Переваги та особливості використання атомів Переваги атомів: Ефективність. Завдяки своєму числовому представленню, порівняння атомів є дуже швидким. Це осо...

Встановлення PostgreSQL на Ubuntu-сервер

Встановлення Оновлюємо пакети та встановлюємо PostgreSQL: sudo apt update sudo apt install -y postgresql postgresql-contrib Перевіряємо статус сервісу: sudo systemctl status postgresql Якщо PostgreSQL не запущений, запустимо його: sudo systemctl start postgresql sudo systemctl enable postgresql Налаштування безпеки Зміна пароля: sudo -u postgres psql У консолі PostgreSQL: ALTER USER postgres PASSWORD 'міцний_пароль'; \q \q - вихід з консолі. Список основних команд для роботи з PostgreSQL можна переглянути за посиланням. За замовчуванням PostgreSQL слухає localhost (127.0.0.1). Щоб дозволити доступ із зовнішніх машин, редагуємо конфігурацію: sudo nano /etc/postgresql/17/main/postgresql.conf (замість 17 вкажи версію PostgreSQL, яку встановлено) Шукаємо рядок: #listen_addresses = 'localhost' та замінюємо на listen_addresses = '*' Зберігаємо (Ctrl + X, Y, Enter). Тепер редагуємо pg_hba.conf: sudo nano /etc/postgresql/17/main/pg_hba.conf...

Основи Node.js

Що таке Node.js? Node.js — це середовище виконання JavaScript поза браузером, побудоване на рушії Google V8 . Воно дозволяє запускати JavaScript на сервері, створюючи серверні застосунки з високою продуктивністю. Основні характеристики: Однопотокова модель з неблокуючим I/O Асинхронне виконання за допомогою event loop Висока продуктивність у роботі з мережевими запитами npm — найбільший реєстр пакетів Що таке event loop? Event loop — це механізм в Node.js, який дозволяє неблокуючим асинхронним операціям виконуватись у середовищі з єдиним потоком. Він постійно перевіряє наявність подій у черзі та викликає відповідні колбеки. Як працює однопоточність у Node.js? Node.js використовує один потік (main thread) для обробки JavaScript-коду. Операції, які займають час (мережеві запити, читання з файлової системи), делегуються до системних API або thread pool, і після завершення результат повертається у основний потік через event loop. Приклад: асинхронна о...