JDBC
Module provides a repository implementation based on the JDBC database protocol and using Hikari for connection set management.
Dependency¶
Dependency build.gradle
:
Module:
Dependency build.gradle.kts
:
Module:
Also required to provide the database driver implementation as a dependency.
Configuration¶
Example of the complete configuration described in the JdbcDatabaseConfig
class (default or example values are specified):
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)!
}
}
}
- JDBC database connection URL (required)
- Username to connect (required)
- Password of the user to connect (required)
- Database schema for the connection
- Name of the database connection set in Hikari (required)
- Maximum size of the database connection set in Hikari
- Minimum size of the set of ready connections to the database in Hikari in standby mode
- Maximum time to establish a connection in Hikari
- Maximum time for connection verification in Hikari
- Maximum time for connection downtime in Hikari
- Maximum lifetime of a connection in Hikari
- Maximum time a connection can be idle in Hikari before it is considered a leak (optional)
- Maximum time to wait for connection initialization at service startup (optional)
- Whether to enable probes.md#_2 for database connection
- Additional JDBC connection attributes
dataSourceProperties
(below examplehostRecheckSeconds
parameters) (optional) - Enables module logging (default
false
) - Enables module metrics (default
true
) - Configures SLO for DistributionSummary metrics
- Enables module tracing (default
true
)
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)!
}
- JDBC database connection URL (required)
- Username to connect (required)
- Password of the user to connect (required)
- Database schema for the connection
- Name of the database connection set in Hikari (required)
- Maximum size of the database connection set in Hikari
- Minimum size of the set of ready connections to the database in Hikari in standby mode
- Maximum time to establish a connection in Hikari
- Maximum time for connection verification in Hikari
- Maximum time for connection downtime in Hikari
- Maximum lifetime of a connection in Hikari
- Maximum time a connection can be idle in Hikari before it is considered a leak (optional)
- Maximum time to wait for connection initialization at service startup (optional)
- Whether to enable probes.md#_2 for database connection
- Additional JDBC connection attributes
dataSourceProperties
(below examplehostRecheckSeconds
parameters) (optional) - Enables module logging (default
false
) - Enables module metrics (default
true
) - Configures SLO for DistributionSummary metrics
- Enables module tracing (default
true
)
Usage¶
Mapping¶
It is possible to override the conversion of different parts of entity and query parameters, Kora provides special interfaces for this.
Result¶
If you need to convert the result manually, it is suggested to use JdbcResultSetMapper
:
final class ResultMapper implements JdbcResultSetMapper<UUID> {
@Override
public UUID apply(ResultSet rs) throws SQLException {
// mapping code
}
}
@Repository
public interface EntityRepository extends JdbcRepository {
@Mapping(ResultMapper.class)
@Query("SELECT id FROM entities")
List<UUID> getIds();
}
Entity¶
Optimal entity mapping intend to use with @EntityJdbc
annotation for result converter generation.
All embedded entities also should use this annotation:
===! “ Java
”
```java
@EntityJdbc
Public record Entity(String id, String name) {}
```
=== “ Kotlin
”
```kotlin
@EntityJdbc
Data class Entity(val id: String, val name: String)
```
Row¶
If you need to convert the string manually, it is suggested to use JdbcRowMapper
:
final class RowMapper implements JdbcRowMapper<UUID> {
@Override
public UUID apply(ResultSet rs) throws SQLException {
return UUID.fromString(rs.getString(0));
}
}
@Repository
public interface EntityRepository extends JdbcRepository {
@Mapping(RowMapper.class)
@Query("SELECT id FROM entities")
List<UUID> findAll();
}
class RowMapper : JdbcRowMapper<UUID> {
@Throws(SQLException::class)
override fun apply(rs: ResultSet): UUID {
return UUID.fromString(rs.getString(0))
}
}
@Repository
interface EntityRepository : JdbcRepository {
@Mapping(RowMapper::class)
@Query("SELECT id FROM entities")
fun findAll(): List<UUID>
}
Column¶
If you need to convert the column value manually, it is suggested to use the JdbcResultColumnMapper
:
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();
}
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>
}
Parameter¶
If you want to convert the value of a query parameter manually, it is suggested to use JdbcParameterColumnMapper
:
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);
}
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>
}
Supported types¶
List of supported types for arguments/return values out of the box
- void
- boolean / Boolean
- short / Short
- int / Integer
- long / Long
- double / Double
- float / Float
- byte[]
- String
- BigDecimal
- UUID
- LocalDate
- LocalTime
- LocalDateTime
- OffsetTime
- OffsetDateTime
Select by list¶
Sometimes a list of values from the database needs to be fetched, all these parameters must be set separately at the driver level, as the length of the list is not known this is not the most obvious task as Kora tries to do all conversions at compile time and remove any string conversions especially in SQL at runtime, such functionality would require adding a separate parameter converter.
What is certain at this point is that it is easy to add support for such parameters without manual connection factory for popular databases like Postgres/Oracle.
Out of the box Kora does not provide conversion of such parameters, but it is easy to add it yourself, an example for Postgres
is shown below:
@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);
}
@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>
}
Generated identifier¶
If you want to get the primary keys of an entity created by the database as the result,
it is suggested to use the @Id
annotation over a method where the return value type is identifiers.
This approach works for @Batch
queries as well.
Transactions¶
In order to execute blocking queries, Kora has a JdbcConnectionFactory
interface, which is provided in a method within the JdbcRepository
contract.
All repository methods called within a transaction lambda will be executed in that transaction.
In order to execute queries transactionally, the inTx
contract can be used:
@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);
});
}
}
- will be executed within the transaction or rolled back if the entire lambda throws an exception
- will be executed within the transaction or rolled back if the entire lambda throws an exception
@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
})
}
}
- will be executed within the transaction or rolled back if the entire lambda throws an exception
- will be executed within the transaction or rolled back if the entire lambda throws an exception
The isolation level is taken from the dsProperties
configuration of the Hikari pool,
or you can change it yourself via java.sql.Connection
before executing queries.
Connection¶
If you need some more complex logic for a query and @Query
is not enough, you can use java.sql.Connection
:
@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);
});
}
}
Signatures¶
Available signatures for repository methods out of the box:
The T
refers to the type of the return value, either List<T>
, either Void
or UpdateCount
.
T myMethod()
@Nullable T myMethod()
Optional<T> myMethod()
CompletionStage<T> myMethod()
CompletionStage (provideExecutor
)Mono<T> myMethod()
Project Reactor (provideExecutor
and add dependency)
By T
we mean the type of the return value, either T?
, either List<T>
, either Unit
or UpdateCount
.
myMethod(): T
suspend myMethod(): T
Kotlin Coroutine (provideExecutor
and add dependency asimplementation
)