Модуль предоставляет реализацию репозиториев на основе JDBC протокола работы с базами данных и с использованием Hikari для управления набором соединений.
===! ":fontawesome-brands-java: Java"
[Зависимость](general.md#_4) `build.gradle`:
```groovy
implementation "ru.tinkoff.kora:database-jdbc"
```
Модуль:
```java
@KoraApp
public interface Application extends JdbcDatabaseModule { }
```
=== ":simple-kotlin: Kotlin"
[Зависимость](general.md#_4) `build.gradle.kts`:
```groovy
implementation("ru.tinkoff.kora:database-jdbc")
```
Модуль:
```kotlin
@KoraApp
interface Application : JdbcDatabaseModule
```
Также требуется предоставить реализацию драйвера базы данных как зависимость.
Пример полной конфигурации, описанной в классе JdbcDatabaseConfig (указаны примеры значений или значения по умолчанию):
===! ":material-code-json: Hocon"
```javascript
db {
jdbcUrl = "jdbc:postgresql://localhost:5432/postgres" //(1)!
username = "postgres" //(2)!
password = "postgres" //(3)!
schema = "public" //(4)!
poolName = "kora" //(5)!
maxPoolSize = 10 //(6)!
minIdle = 0 //(7)!
connectionTimeout = "10s" //(8)!
validationTimeout = "5s" //(9)!
idleTimeout = "10m" //(10)!
maxLifetime = "15m" //(11)!
leakDetectionThreshold = "0s" //(12)!
initializationFailTimeout = "0s" //(13)!
readinessProbe = false //(14)!
dsProperties { //(15)!
"hostRecheckSeconds": "2"
}
telemetry {
logging {
enabled = false //(16)!
}
metrics {
enabled = true //(17)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(18)!
}
tracing {
enabled = true //(19)!
}
}
}
```
1. JDBC URL подключения к базе данных (**обязательный**)
2. Имя пользователя для подключения (**обязательный**)
3. Пароль пользователя для подключения (**обязательный**)
4. Схема базы данных для подключения (по умолчанию отсутвует)
5. Имя набора соединений к базе данных в Hikari (**обязательный**)
6. Максимальный размер набора соединений к базе данных в Hikari
7. Минимальный размер набора готовых соединений к базе данных в Hikari в режиме ожидания
8. Максимальное время на установку соединения в Hikari
9. Максимальное время на проверку соединения в Hikari
10. Максимальное время на простой соединения в Hikari
11. Максимальное время жизни соединения в Hikari
12. Максимальное время соединение может отстуствовать в Hikari до того как будет считаться утечкой (по умолчанию отсутвует)
13. Максимальное время ожидания инициализации соединения при старте сервиса (по умолчанию отсутвует)
14. Включить ли [пробу готовности](probes.md#_2) для соединения базы данных
15. Дополнительные атрибуты JDBC соединения `dataSourceProperties` (ниже пример `hostRecheckSeconds` параметра) (по умолчанию отсутвует)
16. Включает логгирование модуля (по умолчанию `false`)
17. Включает метрики модуля (по умолчанию `true`)
18. Настройка [SLO](https://www.atlassian.com/ru/incident-management/kpis/sla-vs-slo-vs-sli) для [DistributionSummary](https://github.com/micrometer-metrics/micrometer-docs/blob/main/src/docs/concepts/distribution-summaries.adoc) метрики
19. Включает трассировку модуля (по умолчанию `true`)
=== ":simple-yaml: YAML"
```yaml
db:
jdbcUrl: "jdbc:postgresql://localhost:5432/postgres" #(1)!
username: "postgres" #(2)!
password: "postgres" #(3)!
schema: "public" #(4)!
poolName: "kora" #(5)!
maxPoolSize: 10 #(6)!
minIdle: 0 #(7)!
connectionTimeout: "10s" #(8)!
validationTimeout: "5s" #(9)!
idleTimeout: "10m" #(10)!
maxLifetime: "15m" #(11)!
leakDetectionThreshold: "0s" #(12)!
initializationFailTimeout: "0s" //(13)!
readinessProbe: false //(14)!
dsProperties: #(15)!
hostRecheckSeconds: "1"
telemetry:
logging:
enabled: false #(16)!
metrics:
enabled: true #(17)!
slo: [ 2, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(18)!
tracing:
enabled: true #(19)!
}
```
1. JDBC URL подключения к базе данных (**обязательный**)
2. Имя пользователя для подключения (**обязательный**)
3. Пароль пользователя для подключения (**обязательный**)
4. Схема базы данных для подключения (по умолчанию отсутвует)
5. Имя набора соединений к базе данных в Hikari (**обязательный**)
6. Максимальный размер набора соединений к базе данных в Hikari
7. Минимальный размер набора готовых соединений к базе данных в Hikari в режиме ожидания
8. Максимальное время на установку соединения в Hikari
9. Максимальное время на проверку соединения в Hikari
10. Максимальное время на простой соединения в Hikari
11. Максимальное время жизни соединения в Hikari
12. Максимальное время соединение может отстуствовать в Hikari до того как будет считаться утечкой (по умолчанию отсутвует)
13. Максимальное время ожидания инициализации соединения при старте сервиса (по умолчанию отсутвует)
14. Включить ли [пробу готовности](probes.md#_2) для соединения базы данных
15. Дополнительные атрибуты JDBC соединения `dataSourceProperties` (ниже пример `hostRecheckSeconds` параметра) (по умолчанию отсутвует)
16. Включает логгирование модуля (по умолчанию `false`)
17. Включает метрики модуля (по умолчанию `true`)
18. Настройка [SLO](https://www.atlassian.com/ru/incident-management/kpis/sla-vs-slo-vs-sli) для [DistributionSummary](https://github.com/micrometer-metrics/micrometer-docs/blob/main/src/docs/concepts/distribution-summaries.adoc) метрики
19. Включает трассировку модуля (по умолчанию `true`)
===! ":fontawesome-brands-java: Java"
```java
@Repository
public interface EntityRepository extends JdbcRepository { }
```
=== ":simple-kotlin: Kotlin"
```kotlin
@Repository
interface EntityRepository : JdbcRepository
```
Возможно переопределять преобразование различных частей сущности и параметров запроса, для этого Kora предоставляет специальные интерфейсы.
Если требуется преобразовать результат вручную, предлагается использовать JdbcResultSetMapper:
===! ":fontawesome-brands-java: Java"
```java
final class ResultMapper implements JdbcResultSetMapper<UUID> {
@Override
public UUID apply(ResultSet rs) throws SQLException {
// код преобразования
}
}
@Repository
public interface EntityRepository extends JdbcRepository {
@Mapping(ResultMapper.class)
@Query("SELECT id FROM entities")
List<UUID> getIds();
}
```
=== ":simple-kotlin: Kotlin"
Для Kotlin писать преобразователи надо только для `T?` типов, так в интерфейсах тип указан как `@Nullable`.
```kotlin
class ResultMapper : JdbcResultSetMapper<Long> {
@Throws(SQLException::class)
override fun apply(rs: ResultSet): UUID {
// код преобразования
}
}
@Repository
interface EntityRepository : JdbcRepository {
@Mapping(ResultMapper::class)
@Query("SELECT id FROM entities")
fun countIds(): List<UUID>
}
```
Для оптимального преобразование сущности предполагается использовать аннотацию @EntityJdbc
для создания обработчиками аннотаций преобразователя результата.
Для всех вложенных сущностей также предполагается использовать эту аннотацию
===! ":fontawesome-brands-java: Java"
```java
@EntityJdbc
public record Entity(String id, String name) {}
```
=== ":simple-kotlin: Kotlin"
```kotlin
@EntityJdbc
data class Entity(val id: String, val name: String)
```
Если требуется преобразовать строку вручную, предлагается использовать JdbcRowMapper,
имейте в виду, что порядок колонок начинается с 1:
===! ":fontawesome-brands-java: Java"
```java
final class RowMapper implements JdbcRowMapper<UUID> {
@Override
public UUID apply(ResultSet rs) throws SQLException {
return UUID.fromString(rs.getString(1));
}
}
@Repository
public interface EntityRepository extends JdbcRepository {
@Mapping(RowMapper.class)
@Query("SELECT id FROM entities")
List<UUID> findAll();
}
```
=== ":simple-kotlin: Kotlin"
Для Kotlin писать преобразователи надо только для `T?` типов, так в интерфейсах тип указан как `@Nullable`.
```kotlin
class RowMapper : JdbcRowMapper<UUID> {
@Throws(SQLException::class)
override fun apply(rs: ResultSet): UUID {
return UUID.fromString(rs.getString(1))
}
}
@Repository
interface EntityRepository : JdbcRepository {
@Mapping(RowMapper::class)
@Query("SELECT id FROM entities")
fun findAll(): List<UUID>
}
```
Если требуется преобразовать значение колонки вручную, предлагается использовать JdbcResultColumnMapper:
===! ":fontawesome-brands-java: Java"
```java
public final class ColumnMapper implements JdbcResultColumnMapper<UUID> {
@Override
public UUID apply(ResultSet row, int index) throws SQLException {
return UUID.fromString(row.getString(index));
}
}
@EntityJdbc
@Table("entities")
public record Entity(@Mapping(ColumnMapper.class) @Id UUID id, String name) { }
@Repository
public interface EntityRepository extends JdbcRepository {
@Query("SELECT id, name FROM entities")
List<Entity> findAll();
}
```
=== ":simple-kotlin: Kotlin"
Для Kotlin писать преобразователи надо только для `T?` типов, так в интерфейсах тип указан как `@Nullable`.
```kotlin
class ColumnMapper : JdbcResultColumnMapper<UUID> {
@Throws(SQLException::class)
override fun apply(row: ResultSet, index: Int): UUID {
return UUID.fromString(row.getString(index))
}
}
@EntityJdbc
@Table("entities")
data class Entity(
@Id @Mapping(ColumnMapper::class) val id: UUID,
val name: String
)
@Repository
interface EntityRepository : JdbcRepository {
@Query("SELECT id, name FROM entities")
fun findAll(): List<Entity>
}
```
Если требуется преобразовать значение параметра запроса вручную, предлагается использовать JdbcParameterColumnMapper:
===! ":fontawesome-brands-java: Java"
```java
public final class ParameterMapper implements JdbcParameterColumnMapper<UUID> {
@Override
public void set(PreparedStatement stmt, int index, @Nullable UUID value) throws SQLException {
if (value != null) {
stmt.setString(index, value.toString());
}
}
}
@Repository
public interface EntityRepository extends JdbcRepository {
@Query("SELECT id, name FROM entities WHERE id = :id")
List<Entity> findById(@Mapping(ParameterMapper.class) UUID id);
}
```
=== ":simple-kotlin: Kotlin"
Для Kotlin писать преобразователи надо только для `T?` типов, так в интерфейсах тип указан как `@Nullable`.
```kotlin
class ParameterMapper : JdbcParameterColumnMapper<UUID?> {
@Throws(SQLException::class)
override fun set(stmt: PreparedStatement, index: Int, value: UUID?) {
if (value != null) {
stmt.setString(index, value.toString())
}
}
}
@Repository
interface EntityRepository : JdbcRepository {
@Query("SELECT id, name FROM entities WHERE id = :id")
fun findById(@Mapping(ParameterMapper::class) id: UUID): List<Entity>
}
```
??? abstract "Список поддерживаемых типов для аргументов/возвращаемых значений из коробки"
Такие типы выбраны так как поддерживаются большинством популярных баз данных.
* void
* boolean / Boolean
* short / Short
* int / Integer
* long / Long
* double / Double
* float / Float
* byte[]
* String
* BigDecimal
* UUID
* LocalDate
* LocalTime
* LocalDateTime
* OffsetTime
* OffsetDateTime
Иногда требуется выборка по списку значений из базы, на уровне драйвера все эти параметры должны быть отдельно проставлены, так как длина списка не известна это не самая очевидная задача так как Kora старается делать все преобразования во время компиляции и убирать любые преобразования строк особенно в SQL во время выполнения, для такой функциональности потребуется добавить самостоятельный преобразователь параметров.
На данный момент точно известно, что можно легко добавить поддержку таких параметров без ручного управления в такие популярные базы данных как Postgres/Oracle.
Из коробки Kora не предоставляет конвертацию таких параметров, но его легко добавить самостоятельно, ниже показан пример для Postgres:
===! ":fontawesome-brands-java: Java"
```java
@Component
class ListOfStringJdbcParameterMapper implements JdbcParameterColumnMapper<List<String>> {
@Override
public void set(PreparedStatement stmt, int index, List<String> value) throws SQLException {
String[] typedArray = value.toArray(String[]::new);
Array sqlArray = stmt.getConnection().createArrayOf("VARCHAR", typedArray);
stmt.setArray(index, sqlArray);
}
}
@Repository
public interface EntityRepository extends JdbcRepository {
@Query("SELECT id, name FROM entities WHERE id = ANY(:ids)")
List<Entity> findAllByIds(@Mapping(ListOfStringJdbcParameterMapper.class) List<String> ids);
}
```
=== ":simple-kotlin: Kotlin"
```kotlin
@Component
class ListOfStringJdbcParameterMapper : JdbcParameterColumnMapper<List<String>> {
@Throws(SQLException::class)
override fun set(stmt: PreparedStatement, index: Int, value: List<String>) {
val typedArray = value.toTypedArray()
val sqlArray = stmt.connection.createArrayOf("VARCHAR", typedArray)
stmt.setArray(index, sqlArray)
}
}
@Repository
interface EntityRepository : JdbcRepository {
@Query("SELECT id, name FROM entities WHERE id = ANY(:ids)")
fun findAllByIds(@Mapping(ListOfStringJdbcParameterMapper::class) ids: List<String>): List<Entity>
}
```
Если необходимо получить в качестве результата созданные базой данных первичные ключи сущности,
предлагается использовать аннотацию @Id над методом, где тип возвращаемого значения является идентификаторами.
Такой подход работает и для @Batch запросов.
===! ":fontawesome-brands-java: Java"
```java
@Repository
public interface EntityRepository extends JdbcRepository {
@EntityJdbc
public record Entity(Long id, String name) {}
@Query("INSERT INTO entities(name) VALUES (:entity.name)")
@Id
long insert(Entity entity);
}
```
=== ":simple-kotlin: Kotlin"
```kotlin
@Repository
interface EntityRepository : JdbcRepository {
@EntityJdbc
public record Entity(Long id, String name) {}
@Query("INSERT INTO entities(name) VALUES (:entity.name)")
@Id
fun insert(entity: Entity): Long
}
```
Для выполнения блокирующих запросов в Kora есть интерфейс JdbcConnectionFactory,
который предоставляется в методе в рамках контракта JdbcRepository.
Все методы репозитория вызванные в рамках лямбды транзакции будут выполнены в этой самой транзакции.
Для того чтобы выполнять запросы транзакционно, можно использовать контракт inTx:
===! ":fontawesome-brands-java: Java"
```java
@Component
public final class SomeService {
private final EntityRepository repository;
public SomeService(EntityRepository repository) {
this.repository = repository;
}
public List<Entity> saveAll(Entity one, Entity two) {
return repository.getJdbcConnectionFactory().inTx(() -> {
repository.insert(one); //(1)!
// do some work
repository.insert(two); //(2)!
return List.of(one, two);
});
}
}
```
1. Будет выполнено в рамках транзакции либо откатится если вся лямбра выкинет исключение
2. Будет выполнено в рамках транзакции либо откатится если вся лямбра выкинет исключение
=== ":simple-kotlin: Kotlin"
```kotlin
@Component
class SomeService(private val repository: EntityRepository) {
fun saveAll(one: List<Entity>, two: List<Entity>): List<Entity> {
return repository.jdbcConnectionFactory.inTx(SqlFunction1 {
repository.insert(one) //(1)!
// do some work
repository.insert(two) //(2)!
one + two
})
}
}
```
1. Будет выполнено в рамках транзакции либо откатится если вся лямбра выкинет исключение
2. Будет выполнено в рамках транзакции либо откатится если вся лямбра выкинет исключение
Транзакция считается успешно зафиксированной после выполнения метода, если метод не выбросил исключение. В случае если метод выбросил исключение, все изменения в БД в рамках транзакции не будут применены.
Уровень изоляции транзакции берется из конфигурации dsProperties пула Hikari,
либо можно самостоятельно поменять его через java.sql.Connection перед выполнением запросов.
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);Если для запроса нужна какая-то более сложная логика, либо запросы вне репозитория, можно использовать java.sql.Connection:
===! ":fontawesome-brands-java: Java"
```java
@Component
public final class SomeService {
private final EntityRepository repository;
public SomeService(EntityRepository repository) {
this.repository = repository;
}
public List<Entity> saveAll(Entity one, Entity two) {
return repository.getJdbcConnectionFactory().inTx(connection -> {
// do some work
return List.of(one, two);
});
}
}
```
=== ":simple-kotlin: Kotlin"
```kotlin
@Component
class SomeService(private val repository: EntityRepository) {
fun saveAll(one: Entity, two: Entity): List<Entity> {
return repository.jdbcConnectionFactory.inTx(SqlFunction1 { connection: Connection ->
// do some work
listOf(one, two)
})
}
}
```
В случае если требуется выполнить какие-либо действия после фиксации транзакции,
можно добавить соответствущие действия с помощью addPostCommitAction.
===! ":fontawesome-brands-java: Java"
```java
@Component
public final class SomeService {
private final EntityRepository repository;
public SomeService(EntityRepository repository) {
this.repository = repository;
}
public List<Entity> saveAll(Entity one, Entity two) {
return repository.getJdbcConnectionFactory().inTx(connection -> {
var ccc = repository.getJdbcConnectionFactory().currentConnectionContext();
ccc.addPostCommitAction(conn) -> {
// do some work
});
// do some work
return List.of(one, two);
});
}
}
```
=== ":simple-kotlin: Kotlin"
```kotlin
@Component
class SomeService(private val repository: EntityRepository) {
fun saveAll(one: Entity, two: Entity): List<Entity> {
return repository.jdbcConnectionFactory.inTx(SqlFunction1 { connection: Connection ->
val ccc = repository.jdbcConnectionFactory.currentConnectionContext()
ccc.addPostCommitAction { conn -> {
// do some work
}
// do some work
listOf(one, two)
})
}
}
```
В случае если требуется выполнить какие-либо действия после отката транзакции,
можно добавить соответствущие действия с помощью addPostRollbackAction.
===! ":fontawesome-brands-java: Java"
```java
@Component
public final class SomeService {
private final EntityRepository repository;
public SomeService(EntityRepository repository) {
this.repository = repository;
}
public List<Entity> saveAll(Entity one, Entity two) {
return repository.getJdbcConnectionFactory().inTx(connection -> {
var ccc = repository.getJdbcConnectionFactory().currentConnectionContext();
ccc.addPostRollbackAction((conn, e) -> {
// do some work
});
// do some work
return List.of(one, two);
});
}
}
```
=== ":simple-kotlin: Kotlin"
```kotlin
@Component
class SomeService(private val repository: EntityRepository) {
fun saveAll(one: Entity, two: Entity): List<Entity> {
return repository.jdbcConnectionFactory.inTx(SqlFunction1 { connection: Connection ->
val ccc = repository.jdbcConnectionFactory.currentConnectionContext()
ccc.addPostRollbackAction { conn, e ->
// do some work
}
// do some work
listOf(one, two)
})
}
}
```
Доступные сигнатуры для методов репозитория из коробки:
===! ":fontawesome-brands-java: Java"
Под `T` подразумевается тип возвращаемого значения, либо `List<T>`, либо `Void`, либо `UpdateCount`.
- `T myMethod()`
- `@Nullable T myMethod()`
- `Optional<T> myMethod()`
- `CompletionStage<T> myMethod()` [CompletionStage](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/CompletionStage.html) (надо предоставить `Executor`)
- `Mono<T> myMethod()` [Project Reactor](https://projectreactor.io/docs/core/release/reference/) (надо предоставить `Executor` и подключить [зависимость](https://mvnrepository.com/artifact/io.projectreactor/reactor-core))
=== ":simple-kotlin: Kotlin"
Под `T` подразумевается тип возвращаемого значения, либо `T?`, либо `List<T>`, либо `Unit`, либо `UpdateCount`.
- `myMethod(): T`
- `suspend myMethod(): T` [Kotlin Coroutine](https://kotlinlang.org/docs/coroutines-basics.html#your-first-coroutine) (надо предоставить `Executor` и подключить [зависимость](https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core) как `implementation`)