DatabaseClient — це центральний клас в основному пакеті R2DBC. Він обробляє створення та звільнення ресурсів, що допомагає уникнути поширених помилок, наприклад, якщо забути закрити з'єднання. Він виконує базові завдання основного робочого процесу R2DBC (такі як створення та виконання стейтментів), залишаючи код додатка, щоб надавати SQL та отримувати результати. Клас DatabaseClient:

  • Виконує SQL-запити

  • Оновлює інструкції та виклики процедури, що зберігається

  • Виконує обхід екземплярів Result

  • Перехоплює винятки R2DBC і перетворює їх на типізовану, більш інформативну ієрархію винятків, визначену у пакеті org.springframework.dao. (див. Узгоджена ієрархія винятків)

Клієнт має функціональний, вільний API-інтерфейс, який використовує реактивні типи для декларативної компонування.

Якщо ви використовуєте DatabaseClient для свого коду, то необхідно лише реалізувати інтерфейси java.util.function, надавши їм чітко визначений контракт. Враховуючи Connection, наданий класом DatabaseClient, зворотний виклик Function створює Publisher. Те саме стосується і функцій відображення, які отримують результат Row.

Можна використовувати DatabaseClient всередині реалізації DAO шляхом прямого створення екземпляра з посиланням на ConnectionFactory, або ж можна конфігурувати його в IoC-контейнері Spring і передати його DAO-об'єктам як посилання на бін.

Найпростіший спосіб створення об'єкта DatabaseClient — це статичний фабричний метод, як показано нижче:

Java
DatabaseClient client = DatabaseClient.create(connectionFactory);
Kotlin
val client = DatabaseClient.create(connectionFactory)
ConnectionFactory завжди потрібно конфігурувати в IoC-контейнері Spring як бін.

Попередній метод створює DatabaseClient із налаштуваннями за замовчуванням.

Також можна отримати екземпляр Builder з DatabaseClient.builder(). Можна налаштувати клієнт, якщо викликати такі методи:

  • ….bindMarkers(…): вкажіть конкретну фабрику BindMarkersFactory, щоб налаштувати іменований параметр для перетворення маркерів прив'язки бази даних.

  • ….executeFunction(…): встановіть ExecuteFunction, відповідно до якої будуть виконуватися об'єкти Statement.

  • ….namedParameters(false): вимкніть розширення іменованих параметрів. Увімкнено за замовчуванням.

Діалекти визначаються BindMarkersFactoryResolver з ConnectionFactory , зазвичай шляхом перевірки ConnectionFactoryMetadata. Ти можеш дозволити Spring автоматично виявляти свою BindMarkersFactory, зареєструвавши клас, що реалізує org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProvider через META-INF factories. BindMarkersFactoryResolver виявляє реалізації постачальників маркерів прив'язки з classpath за допомогою використання SpringFactoriesLoader.

Наразі підтримуються такі бази даних:

  • H2

  • MariaDB

  • Microsoft SQL Server

  • MySQL

  • Postgres

Всі SQL-запити, що видаються цим класом, реєструються на рівні DEBUG під категорією, що відповідає повному імені класу екземпляра клієнта (зазвичай DefaultDatabaseClient). До того ж, кожне виконання реєструє контрольну точку реактивної послідовності для полегшення налагодження.

У наступних розділах наведено деякі приклади використання DatabaseClient. Ці приклади не є вичерпним списком всіх функціональних можливостей, що надаються DatabaseClient. Див. про це у супутньому javadoc.

Виконання стейтментів

DatabaseClient забезпечує базову функціональність виконання стейтмента. У цьому прикладі показано, що має бути у складі мінімального, але повністю функціонального коду, який створює нову таблицю:

Java

Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
    .then();
Kotlin

client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
    .await()

DatabaseClient розроблений для зручного, вільного використання. Він розкриває проміжні методи, методи, що продовжують та кінцеві методи на кожному етапі специфікації виконання. У попередньому прикладі використовується then() для повернення Publisher, який завершується, щойно запит (або запити, якщо SQL-запит містить кілька стейтментів) завершиться.

execute(…) приймає або рядок SQL-запиту, або запит Supplier<String>, щоб відкласти фактичне створення запиту до його виконання.

Побудова запитів (SELECT)

SQL-запити можуть повертати значення через об'єкти Row або кількість рядків, яких це стосуєтья. DatabaseClient може повернути кількість оновлених рядків або самі рядки, залежно від виданого запиту.

Наступний запит отримує стовпці id та name з таблиці:

Java

Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person")
        .fetch().first();
Kotlin

val first = client.sql("SELECT id, name FROM person")
        .fetch().awaitSingle()

У наступному запиті використовується змінна зв'язування:

Java

Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
        .bind("fn", "Joe")
        .fetch().first();
Kotlin

val first = client.sql("SELECT id, name FROM person WHERE WHERE first_name = :fn")
        .bind("fn", "Joe")
        .fetch().awaitSingle()

Можна помітити використання fetch() у прикладі вище. fetch() — це продовжувальний оператор, який дозволяє зазначити, скільки даних потрібно спожити.

Виклик first() повертає перший рядок з результату і відкидає залишок рядків. Дані можна використовувати за допомогою наступних операторів:

  • first() повертає перший рядок всього результату. Його різновид співпрограми на Kotlin називається awaitSingle() для повертаємих значень, що не допускають порожнього значення, і awaitSingleOrNull(), якщо значення необов'язкове.

  • one() повертає рівно один результат і завершується помилкою, якщо результат містить більше рядків. Використовуються співпрограми Kotlin, а саме awaitOne() для отримання рівно одного значення або awaitOneOrNull(), якщо значення може бути null.

  • all() повертає всі рядки результату. При використанні співпрограм на Kotlin застосовуй flow().

  • rowsUpdated() повертає кількість зачеплених рядків (лічильник INSERT/UPDATE/DELETE). Його варіант співпрограми на Kotlin називається awaitRowsUpdated().

Без зазначення подальших деталей відображення запити повертають табличні результати у вигляді Map, ключами яких є імена стовпців без урахування регістру, які відображаються в значення стовпців. Можна управляти поданням результатів, якщо надати Function >, щоб вона могла повертати довільні значення (поодинокі значення, колекції, Map і об'єкти).

Java

Flux<String> names = client.sql("SELECT name FROM person")
        .map(row -> row.get("name", String.class))
        .all();
Kotlin

val names = client.sql("SELECT name FROM person")
        .map{ row: Row -> row.get("name", String.class) }
        .flow()
А що стосовно null?

Результати реляційної бази даних можуть містити значення null. Специфікація Reactive Streams забороняє передачу значень null. Ця вимога передбачає належну обробку null у функції екстрактора. Хоча і можна отримувати значення null з Row, не потрібно породжувати null значення. Потрібно обернути всі значення null в об'єкт (наприклад, Optional для одиничних значень), щоб переконатися, що значення null ніколи не буде повернене безпосередньо функцією екстрактора.

Оновлення (INSERT, UPDATE та DELETE) за допомогою DatabaseClient

Єдина відмінність модифікуючих інструкцій полягає в тому, що ці інструкції зазвичай не повертають табличні дані, тому для отримання результатів використовується функція rowsUpdated().

У наступному прикладі показано інструкцію UPDATE, яка повертає кількість оновлених рядків:

Java

Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn")
        .bind("fn", "Joe")
        .fetch().rowsUpdated();
Kotlin

val affectedRows = client.sql("UPDATE person SET first_name = :fn")
        .bind("fn", "Joe")
        .fetch(). awaitRowsUpdated()

Прив'язка значень до запитів

Типова програма вимагає параметризованих SQL-інструкцій для вибірки або оновлення рядків відповідно до певних вхідних даними. Зазвичай це інструкції SELECT, обмежені виразом WHERE, або інструкції INSERT та UPDATE, які приймають вхідні параметри. Параметризовані інструкції несуть ризик застосування SQL, якщо параметри не приведені до належного вигляду. DatabaseClient використовує API-інтерфейс bind з R2DBC для усунення ризику впровадження SQL для параметрів запиту. Можна вказати параметризовану SQL-інструкцію за допомогою оператора execute(…) та прив'язати параметри до фактичної Statement. Потім драйвер R2DBC запускає стейтмент за допомогою скомпільованого стейтмента та підстановки параметрів.

Прив'язка параметрів підтримує дві стратегії прив'язки:

  • За індексом, використовуючи нульові індекси параметрів.

  • На ім'я, використовуючи ім'я-плейсхолдер.

У наступному прикладі показано прив'язування параметрів для запиту:


db.sql("INSERT INTO person (id, name , age) VALUES(:id, :name, :age)")
    .bind("id", "joe")
    .bind("name", "Joe")
    .bind("age", 34);
Нативні маркери прив'язки R2DBC

R2DBC використовує нативні маркери прив'язки бази даних, які залежать від фактичного виробника бази даних. Як приклад, у Postgres використовуються індексовані маркери, такі як $1, $2, $n. Іншим прикладом є SQL Server, який використовує іменовані маркери зв'язування з префіксом @.

Це відрізняється від JDBC, який вимагає ? як маркер прив'язки. У JDBC фактичні драйвери перетворюють маркери прив'язки ? на нативні маркери бази даних у процесі виконання інструкції. Підтримка R2DBC в Spring Framework дозволяє використовувати нативні маркери прив'язки або іменовані маркери прив'язки з синтаксисом >:name.

Засоби підтримки іменованих параметрів використовують екземпляр BindMarkersFactory для розширення іменованих параметрів до нативних маркерів прив'язки під час виконання запиту, що забезпечує певний ступінь переносності запитів для різних виробників баз даних.

Препроцесор запитів розгортає іменовані параметри Collection у серію маркерів прив'язки, щоб усунути необхідність динамічного створення запиту залежно від кількості аргументів. Вкладені масиви об'єктів розширюються, дозволяючи використовувати (наприклад) списки, що розкриваються.

Розглянемо наступний запит:

SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50)) 

Попередній запит можна налаштувати та виконати таким чином:

Java

List<Object []> tuples = new ArrayList<>();
tuples.add(new Object[] {"John", 35});
tuples.add(new Object[] {"Ann", 50});
client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
    .bind("tuples", tuples);
Kotlin

val tuples: MutableList<Array<Any>> = ArrayList()
tuples.add(arrayOf("John", 35))
tuples.add(arrayOf("Ann", 50))
client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
    .bind("tuples", tuples)
Можливість використання списків, що розкриваються залежить від виробника.

У наступному прикладі показаний простіший варіант з використанням предикатів IN:

Java

client.sql(" SELECT id, name, state FROM table WHERE age IN (:ages)")
    .bind("ages", Arrays.asList(35, 50));
Kotlin
val tuples: MutableList<Array<Any>>
            = ArrayList() tuples.add(arrayOf("John", 35)) tuples.add(arrayOf("Ann", 50)) client.sql("SELECT id, name,
            state FROM table WHERE age IN (:ages) ") .bind("tuples", arrayOf(35, 50))
Сам R2DBC не підтримує значення типу Collection. Тим не менш, розширення цього List у наведеному вище прикладі працює для іменованих параметрів у засобах підтримки Spring для R2DBC, наприклад, для використання у виразах IN, як показано вище. Однак для вставки або оновлення стовпців з типом масиву (наприклад, Postgres) потрібен тип масиву, який підтримується базовим драйвером R2DBC: зазвичай це масив Java, наприклад, String[] для оновлення стовпця text[]. Не передавай Collection<String> як параметр масиву.

Фільтри стейтментів

Іноді потрібно тонко налаштувати опції для самої Statement, перш ніж вона запуститься. Зареєструй фільтр Statement (StatementFilterFunction) через DatabaseClient для перехоплення та модифікації інструкцій у процесі їх виконання, як показано в наступному прикладі:

Java

client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter((s, next) -> next. execute(s.returnGeneratedValues("id")))
    .bind("name", …)
    .bind("state", …);
Kotlin

client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter { s: Statement, next: ExecuteFunction -> next.execute(s.returnGeneratedValues("id")) }
    .bind("name", …)
    .bind("state", …)

DatabaseClient відкриває також спрощене навантаження filter(…), яке приймає Function<Statement, Statement>:

Java

client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter(statement -> s.returnGeneratedValues("id"));
client.sql("SELECT id, name, state FROM table")
    .filter(statement -> s.fetchSize(25));
Kotlin

client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter { statement -> s.returnGeneratedValues("id") }
client.sql("SELECT id, name, state FROM table")
    .filter { statement -> s.fetchSize(25) }

Реалізації StatementFilterFunction дозволяють фільтрувати Statement, а також фільтрувати об'єкти Result.

Оптимальні методи роботи з DatabaseClient

Після конфігурування екземпляри класу DatabaseClient є потокобезпечними. Це важливо, оскільки означає, що можна налаштувати один екземпляр DatabaseClient, а потім безпечно впроваджувати це загальне посилання до кількох DAO (або репозиторіїв). DatabaseClient зберігає стан, оскільки зберігає посилання на ConnectionFactory, але цей стан не є діалоговим станом.

Загальноприйнятим методом роботи під час використання класу DatabaseClient є конфігурація ConnectionFactory у конфігураційному файлі Spring, а потім впровадження залежностей із цим загальним біном ConnectionFactory до класів DAO. DatabaseClient створюється в сетері для ConnectionFactory. Це призводить до появи DAO приблизно такого вигляду:

Java

public class R2dbcCorporateEventDao implements CorporateEventDao {
    private DatabaseClient databaseClient;
    public void setConnectionFactory(ConnectionFactory connectionFactory) {
        this.databaseClient = DatabaseClient.create(connectionFactory);
    }
    // Реалізації методів для CorporateEventDao за допомогою R2DBC слідують...
}
Kotlin

class R2dbcCorporateEventDao(connectionFactory : ConnectionFactory) : CorporateEventDao {
    private val databaseClient = DatabaseClient.create(connectionFactory)
    // Реалізації методів для CorporateEventDao з підтримкою R2DBC слідують...
}

Альтернативою явній конфігурації є використання сканування компонентів та підтримка інструкцій для застосування залежностей. У цьому випадку можна позначити клас за допомогою анотації @Component (що робить його кандидатом на сканування компонентів) та анотувати метод встановлення ConnectionFactory за допомогою @Autowired. У цьому прикладі показано, як це зробити:

Java

@Component 
public class R2dbcCorporateEventDao implements CorporateEventDao {
    private DatabaseClient databaseClient;
    @Autowired
    public void setConnectionFactory(ConnectionFactory connectionFactory) {
        this.databaseClient = DatabaseClient.create(connectionFactory); 
    }
    // Реалізації методів для CorporateEventDao за допомогою R2DBC слідують...
}
  1. Анотуємо клас за допомогою @Component.
  2. Анотуємо сетер ConnectionFactory за допомогою @Autowired.
  3. Створюємо новий DatabaseClient за допомогою ConnectionFactory.
Kotlin

@Component 
class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao { 
    private val databaseClient = DatabaseClient(connectionFactory) 
    // Реалізації методів для CorporateEventDao з підтримкою R2DBC слідують...
}
  1. Анотуємо клас за допомогою @Component.
  2. Constructor injection of the ConnectionFactory.
  3. Створюємо новий DatabaseClient за допомогою ConnectionFactory .

Незалежно від того, який із описаних вище стилів ініціалізації шаблону ти вирішиш використовувати (або не використовувати), рідко виникає необхідність створювати новий екземпляр класу DatabaseClient щоразу, коли необхідно виконати SQL. Після конфігурування екземпляр DatabaseClient є безпечним. Якщо твій додаток звертається до кількох баз даних, може знадобитися кілька екземплярів DatabaseClient, що потребує наявності кількох ConnectionFactory і, відповідно, кількох по-різному налаштованих екземплярів DatabaseClient.

Отримання автоматично згенерованих ключів

Інструкції INSERT можуть генерувати ключі під час вставки рядків до таблиці, які визначають стовпець з автоінкрементом або ідентифікатором. Щоб отримати повний контроль над ім'ям стовпця, що генерується, просто зареєструй StatementFilterFunction, яка буде робити запит на згенерований ключ для потрібного стовпця.

Java

Mono<Integer> generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter(statement -> s.returnGeneratedValues("id"))
            .map(row -> row. get("id", Integer.class))
            .first();
// generatedId породжує згенерований ключ після завершення інструкції INSERT
Kotlin

val generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter { statement -> s.returnGeneratedValues("id") }
        .map { row -> row.get("id", Integer.class) }
        .awaitOne()
// generatedId породжує згенерований ключ після завершення інструкції INSERT