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, ключами яких є імена стовпців без урахування регістру, які відображаються в значення стовпців. >Row, щоб вона могла повертати довільні значення (поодинокі значення, колекції, 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 запускає стейтмент за допомогою скомпільованого стейтмента та підстановки параметрів.

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

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

    p>

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

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

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 фактичні драйвери перетворюють маркери прив'язки ? на нативні маркери бази даних у процесі виконання інструкції. >: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