Micronaut SQL Libraries

Projects to support SQL Database access in Micronaut

Version: 7.3.0-SNAPSHOT

1 Introduction

This project includes modules to support SQL database access in Micronaut.

Available integrations include:

  • JDBC connection pool integrations for Apache DBCP2, HikariCP, Tomcat JDBC, and Oracle UCP

  • ORM integrations for Hibernate ORM and Hibernate Reactive

  • Query and mapper integrations for Jdbi, jOOQ, and MyBatis

  • Reactive SQL client integrations for Vert.x MySQL and PostgreSQL clients

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 Breaking Changes

Micronaut SQL 5.8.0

In Micronaut SQL 5.7.0, you could disable data sources if you set datasources.enabled to false. Since Micronaut SQL 5.8.0, it is no longer supported. If you have multiple data sources, you have to disable them all individually. For example if you had two data sources named a and b, you have to set datasources.a.enabled=false and datasources.b.enabled=false.

4 Configuring JDBC

Java data sources can be configured for one of five currently provided implementations. Apache DBCP2, Hikari, Tomcat, Oracle Universal Connection Pool, and an unpooled DriverManager-backed datasource are supported by default.

Using the CLI

If you are creating your project using the Micronaut CLI, supply one of the jdbc-tomcat, jdbc-hikari, jdbc-dbcp, or jdbc-ucp features to preconfigure a simple JDBC connection in your project, along with a default H2 database driver:

$ mn create-app my-app --features jdbc-tomcat

To get started, simply add a dependency to one of the JDBC configurations that corresponds to the implementation you would like to use. Choose one of the following:

runtimeOnly("io.micronaut.sql:micronaut-jdbc-tomcat")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jdbc-tomcat</artifactId>
    <scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-jdbc-tomcat",
]

runtimeOnly("io.micronaut.sql:micronaut-jdbc-hikari")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jdbc-hikari</artifactId>
    <scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-jdbc-hikari",
]

runtimeOnly("io.micronaut.sql:micronaut-jdbc-dbcp")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jdbc-dbcp</artifactId>
    <scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-jdbc-dbcp",
]

runtimeOnly("io.micronaut.sql:micronaut-jdbc-ucp")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jdbc-ucp</artifactId>
    <scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-jdbc-ucp",
]

runtimeOnly("io.micronaut.sql:micronaut-jdbc-unpooled")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jdbc-unpooled</artifactId>
    <scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-jdbc-unpooled",
]

You also need to add a JDBC driver dependency to your classpath. For example to add the H2 In-Memory Database:

runtimeOnly("com.h2database:h2")
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "com.h2database:h2",
]

To configure Micronaut-managed JDBC transactions without Spring, see Transaction Management.

4.1 Disable Micronaut JDBC Data Sources

You can disable Micronaut Data Sources, for example in a test, by setting datasources.<datasource-name>.enabled to false.

Please note that such test should have @MicronautTest(transactional = false) because required transaction beans won’t be available because there is no datasource and connection available in the context.

Micronaut SQL 5.7.0 allowed disabling datasources via setting datasources.enabled to `false. That it is no longer supported. If you have multiple datasources you have to disable them all individually.

4.2 Configuring JDBC Connection Pools

All of the implementation specific parameters can be configured. Effort was made to allow basic configuration to be consistent across the implementations.

If you want Micronaut to avoid connection pooling entirely, add the micronaut-jdbc-unpooled module instead. It uses the same common datasource settings, creates a new physical JDBC connection for each getConnection() call, and closes that physical connection when the caller closes it.

  • Hikari: The URL is able to be configured through url in addition to jdbcUrl. The JNDI name can be configured through jndiName in addition to dataSourceJNDI.

  • Tomcat: The JNDI name can be configured through jndiName in addition to dataSourceJNDI.

Several configuration options will be calculated if they are not provided.

URL

The classpath will be searched for an embedded database driver. If found, the URL will be set to the default value for that driver.

Driver Class

If the URL is configured, the driver class will be derived from the URL, otherwise the classpath will be searched for an embedded database driver. If found, the default class name for that driver will be used.

Username

If the configured database driver is embedded, the username will be set to "sa"

Password

If the configured database driver is embedded, the password will be set to an empty string.

For example:

datasources.default: {}
["datasources.default"]
datasources.default {
}
{
  "datasources.default" {
  }
}
{
  "datasources.default": {
  }
}

The above configuration will result in a single DataSource bean being registered with the named qualifier of default.

If for example, the H2 driver is on the classpath, it is equivalent to the following:

datasources.default.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
datasources.default.username=sa
datasources.default.password=
datasources.default.driverClassName=org.h2.Driver
datasources:
    default:
        url: jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
        username: sa
        password: ""
        driverClassName: org.h2.Driver
[datasources.default]
url = "jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"
username = "sa"
password = ""
driverClassName = "org.h2.Driver"
datasources {
  'default' {
    url = "jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"
    username = "sa"
    password = ""
    driverClassName = "org.h2.Driver"
  }
}
{
  datasources {
    default {
      url = "jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"
      username = "sa"
      password = ""
      driverClassName = "org.h2.Driver"
    }
  }
}
{
  "datasources": {
    "default": {
      "url": "jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
      "username": "sa",
      "password": "",
      "driverClassName": "org.h2.Driver"
    }
  }
}

To use Oracle UCP, provide a configuration similar to the following:

datasources.default.url=null
datasources.default.connectionFactoryClassName=oracle.jdbc.pool.OracleDataSource
datasources.default.username=null
datasources.default.password=null
datasources.default.minPoolSize=1
datasources.default.maxPoolSize=10
datasources:
  default:
    url:
    connectionFactoryClassName: oracle.jdbc.pool.OracleDataSource
    username:
    password:
    minPoolSize: 1
    maxPoolSize: 10
[datasources.default]
connectionFactoryClassName = "oracle.jdbc.pool.OracleDataSource"
minPoolSize = 1
maxPoolSize = 10
datasources {
  'default' {
    url = null
    connectionFactoryClassName = "oracle.jdbc.pool.OracleDataSource"
    username = null
    password = null
    minPoolSize = 1
    maxPoolSize = 10
  }
}
{
  datasources {
    default {
      url = null
      connectionFactoryClassName = "oracle.jdbc.pool.OracleDataSource"
      username = null
      password = null
      minPoolSize = 1
      maxPoolSize = 10
    }
  }
}
{
  "datasources": {
    "default": {
      "url": null,
      "connectionFactoryClassName": "oracle.jdbc.pool.OracleDataSource",
      "username": null,
      "password": null,
      "minPoolSize": 1,
      "maxPoolSize": 10
    }
  }
}

The Oracle UCP is managed by the UniversalConnectionPoolManager. The manager can be disabled by setting ucp-manager.enabled to false. Additionally, you can enable the JMX-Based Management by setting the ucp-manager.jmx.enabled to true. When using current Oracle JDBC 23ai driver versions, the extra ojdbc*dms.jar driver artifacts are not required.

The micronaut-jdbc-ucp module uses Oracle’s ojdbc17 and ucp17 artifacts by default. Applications that need the JDK 11 driver line must exclude those default transitive dependencies and add direct dependencies on Oracle’s ojdbc11 and ucp11 artifacts instead. The ojdbc11 and ucp11 artifacts remain managed, so after excluding ojdbc17 and ucp17, you can rely on Micronaut’s managed versions for the replacement dependencies.

In case when exception Universal Connection Pool already exists in the Universal Connection Pool Manager is thrown, there is oracle.ucp.destroyOnReload configuration that can be set to true to avoid this error. In that case, when creating new connection pool, if there is the one with the same name the old one will be destroyed first.

There is also property oracle.ucp.createConnectionInBorrowThread that can control connection creation using background threads. The default configuration since UCP 23.x is that connections are created using background threads instead of user threads. Adding configuration oracle.ucp.createConnectionInBorrowThread=true will switch to the old behavior. More about it can be read here Borrowing Connections from UCP.

UCP lifecycle settings are configured like any other pool property. For example, to recycle connections before a firewall idle timeout and to enable connection harvesting:

datasources.default.maxConnectionReuseTime=300
datasources.default.maxConnectionReuseCount=1000
datasources.default.connectionHarvestTriggerCount=5
datasources.default.connectionHarvestMaxCount=2
datasources:
  default:
    maxConnectionReuseTime: 300
    maxConnectionReuseCount: 1000
    connectionHarvestTriggerCount: 5
    connectionHarvestMaxCount: 2
[datasources.default]
maxConnectionReuseTime = 300
maxConnectionReuseCount = 1000
connectionHarvestTriggerCount = 5
connectionHarvestMaxCount = 2
datasources {
  'default' {
    maxConnectionReuseTime = 300
    maxConnectionReuseCount = 1000
    connectionHarvestTriggerCount = 5
    connectionHarvestMaxCount = 2
  }
}
{
  datasources {
    default {
      maxConnectionReuseTime = 300
      maxConnectionReuseCount = 1000
      connectionHarvestTriggerCount = 5
      connectionHarvestMaxCount = 2
    }
  }
}
{
  "datasources": {
    "default": {
      "maxConnectionReuseTime": 300,
      "maxConnectionReuseCount": 1000,
      "connectionHarvestTriggerCount": 5,
      "connectionHarvestMaxCount": 2
    }
  }
}

Some UCP settings take objects rather than values, such as a connection creation consumer, a connection labeling callback or a custom access token supplier. Configure these with a bean that implements PoolDataSourceCustomizer. OCI IAM and Azure AD tokens need no code: use the token providers of the Oracle JDBC Driver Extensions. Every customizer is applied to every UCP data source, in Ordered order, after configuration and before the pool starts. Use the data source name to target one:

import io.micronaut.configuration.jdbc.ucp.PoolDataSourceCustomizer;
import jakarta.inject.Singleton;
import oracle.ucp.ConnectionCreationInformation;
import oracle.ucp.jdbc.PoolDataSource;

import java.util.concurrent.atomic.AtomicInteger;

@Singleton
public class ConnectionCreationCounter implements PoolDataSourceCustomizer {

    private final AtomicInteger created = new AtomicInteger();

    @Override
    public void customize(String dataSourceName, PoolDataSource pool) { // (1)
        if ("default".equals(dataSourceName)) {
            pool.registerConnectionCreationConsumer(this::onCreated); // (2)
        }
    }

    private void onCreated(ConnectionCreationInformation info) {
        if (info.getStatus() == ConnectionCreationInformation.Status.SUCCESS) {
            created.incrementAndGet();
        }
    }

    public int getCreated() {
        return created.get();
    }
}
from java.util.concurrent.atomic import AtomicInteger

from micronaut.configuration.jdbc.ucp import PoolDataSourceCustomizer
from jakarta.inject import Singleton
from oracle.ucp import ConnectionCreationInformation
from oracle.ucp.jdbc import PoolDataSource

@Singleton
class ConnectionCreationCounter(PoolDataSourceCustomizer):
    def __init__(self) -> None:
        self._created = AtomicInteger()

    def customize(self, data_source_name: str, pool: PoolDataSource) -> None:  # (1)
        if data_source_name == "default":
            pool.registerConnectionCreationConsumer(self._on_created)  # (2)

    def _on_created(self, info: ConnectionCreationInformation) -> None:
        if info.getStatus() == ConnectionCreationInformation.Status.SUCCESS:
            self._created.incrementAndGet()

    def created(self) -> int:
        return self._created.get()
import io.micronaut.configuration.jdbc.ucp.PoolDataSourceCustomizer
import jakarta.inject.Singleton
import oracle.ucp.ConnectionCreationInformation
import oracle.ucp.jdbc.PoolDataSource
import java.util.concurrent.atomic.AtomicInteger

@Singleton
class ConnectionCreationCounter : PoolDataSourceCustomizer {

    private val created = AtomicInteger()

    override fun customize(dataSourceName: String, pool: PoolDataSource) { // (1)
        if (dataSourceName == "default") {
            pool.registerConnectionCreationConsumer { info -> // (2)
                if (info.status == ConnectionCreationInformation.Status.SUCCESS) {
                    created.incrementAndGet()
                }
            }
        }
    }

    fun created(): Int = created.get()
}
import io.micronaut.configuration.jdbc.ucp.PoolDataSourceCustomizer
import jakarta.inject.Singleton
import oracle.ucp.ConnectionCreationInformation
import oracle.ucp.jdbc.PoolDataSource

import java.util.concurrent.atomic.AtomicInteger

@Singleton
class ConnectionCreationCounter implements PoolDataSourceCustomizer {

    private final AtomicInteger createdCount = new AtomicInteger()

    @Override
    void customize(String dataSourceName, PoolDataSource pool) { // (1)
        if (dataSourceName == "default") {
            pool.registerConnectionCreationConsumer { ConnectionCreationInformation info -> // (2)
                if (info.status == ConnectionCreationInformation.Status.SUCCESS) {
                    createdCount.incrementAndGet()
                }
            }
        }
    }

    int getCreated() {
        createdCount.get()
    }
}
1 The customizer is called for each UCP data source, with its name, before the pool starts.
2 Registers a connection creation consumer, one of the settings configuration cannot supply. A token supplier for OCI IAM or Azure AD authentication is set the same way, with pool.setTokenSupplier(…​).
Prefer PoolDataSourceCustomizer over a BeanCreatedEventListener<PoolDataSource>. Other listeners, such as the one Micronaut Data registers, can wrap the DataSource bean before such a listener runs.

To use Database Resident Connection Pooling (DRCP), append :POOLED to the service name of the URL (or add (SERVER=POOLED) to a long connect descriptor) and optionally name the connection class. UCP uses the pool name as the connection class when none is set. Add (POOL_NAME=<name>) to the connect descriptor to use one of the named pools of a multi-pool DRCP.

datasources.default.url=jdbc:oracle:thin:@dbhost:1521/orders:POOLED
datasources.default.connectionFactoryClassName=oracle.jdbc.pool.OracleDataSource
datasources.default.data-source-properties.oracle.jdbc.DRCPConnectionClass=orders
datasources:
  default:
    url: jdbc:oracle:thin:@dbhost:1521/orders:POOLED
    connectionFactoryClassName: oracle.jdbc.pool.OracleDataSource
    data-source-properties:
      oracle.jdbc.DRCPConnectionClass: orders
[datasources.default]
url = "jdbc:oracle:thin:@dbhost:1521/orders:POOLED"
connectionFactoryClassName = "oracle.jdbc.pool.OracleDataSource"

[datasources.default.data-source-properties]
"oracle.jdbc.DRCPConnectionClass" = "orders"
datasources {
  'default' {
    url = "jdbc:oracle:thin:@dbhost:1521/orders:POOLED"
    connectionFactoryClassName = "oracle.jdbc.pool.OracleDataSource"
    dataSourceProperties {
      oracle.jdbc.DRCPConnectionClass = "orders"
    }
  }
}
{
  datasources {
    default {
      url = "jdbc:oracle:thin:@dbhost:1521/orders:POOLED"
      connectionFactoryClassName = "oracle.jdbc.pool.OracleDataSource"
      data-source-properties {
        "oracle.jdbc.DRCPConnectionClass" = "orders"
      }
    }
  }
}
{
  "datasources": {
    "default": {
      "url": "jdbc:oracle:thin:@dbhost:1521/orders:POOLED",
      "connectionFactoryClassName": "oracle.jdbc.pool.OracleDataSource",
      "data-source-properties": {
        "oracle.jdbc.DRCPConnectionClass": "orders"
      }
    }
  }
}

Oracle Database 23ai and later also support implicit connection pooling, which releases the pooled server at statement or transaction boundaries without application calls. It works with DRCP or Proxy Resident Connection Pooling, so the connect descriptor needs (SERVER=POOLED) as well as (POOL_BOUNDARY=STATEMENT) or (POOL_BOUNDARY=TRANSACTION):

datasources.default.url=jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=dbhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=orders)(SERVER=POOLED)(POOL_BOUNDARY=TRANSACTION)))
datasources.default.connectionFactoryClassName=oracle.jdbc.pool.OracleDataSource
datasources:
  default:
    url: jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=dbhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=orders)(SERVER=POOLED)(POOL_BOUNDARY=TRANSACTION)))
    connectionFactoryClassName: oracle.jdbc.pool.OracleDataSource
[datasources.default]
url = "jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=dbhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=orders)(SERVER=POOLED)(POOL_BOUNDARY=TRANSACTION)))"
connectionFactoryClassName = "oracle.jdbc.pool.OracleDataSource"
datasources {
  'default' {
    url = "jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=dbhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=orders)(SERVER=POOLED)(POOL_BOUNDARY=TRANSACTION)))"
    connectionFactoryClassName = "oracle.jdbc.pool.OracleDataSource"
  }
}
{
  datasources {
    default {
      url = "jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=dbhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=orders)(SERVER=POOLED)(POOL_BOUNDARY=TRANSACTION)))"
      connectionFactoryClassName = "oracle.jdbc.pool.OracleDataSource"
    }
  }
}
{
  "datasources": {
    "default": {
      "url": "jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=dbhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=orders)(SERVER=POOLED)(POOL_BOUNDARY=TRANSACTION)))",
      "connectionFactoryClassName": "oracle.jdbc.pool.OracleDataSource"
    }
  }
}

STATEMENT releases the server whenever the session is stateless, and TRANSACTION when a transaction ends or the session is stateless. When the connect descriptor sets POOL_BOUNDARY, it takes precedence over the pooling APIs: a release call on a session that was already released at a boundary does nothing, and a release call on a session that still holds state releases it.

Oracle can also reset the session state at the end of each request with the RESET_STATE service attribute. A database administrator sets it on the service, with DBMS_SERVICE or srvctl. With LEVEL1, Oracle cancels cursors, clears PL/SQL package state and truncates session temporary tables. With LOGIN, it also restores the login-time settings. The default is NONE.

Which values are available depends on the release:

  • LOGIN is new in Oracle Database 26ai, and the Oracle JDBC driver supports it from 23.26.2. Earlier releases only offer NONE and LEVEL1.

  • Before 26ai, RESET_STATE depends on Application Continuity: on 21c and 23ai, it needs a service with Application Continuity or Transparent Application Continuity enabled. 26ai removed that dependency.

  • With DRCP, or when connecting through Oracle Connection Manager in Traffic Director Mode (CMAN-TDM) 23.26.4, LOGIN behaves like LEVEL1.

RESET_STATE needs explicit request boundaries. UCP marks them whenever a connection is borrowed and returned. With other pools, Micronaut Data 5.3.0 and later marks them when datasources.<name>.request-boundaries is true. RESET_STATE does not apply to the implicit boundaries of POOL_BOUNDARY, nor to the implicit request boundaries that Transparent Application Continuity adds. With UCP, set validateConnectionOnBorrow: true when the service combines RESET_STATE=LEVEL1 with Application Continuity. Without DRCP or CMAN-TDM, LOGIN is an alternative; with either, it behaves like LEVEL1, so set validateConnectionOnBorrow: true there as well. LEVEL1 cannot be combined with UCP connection labeling.

To connect to an Oracle sharded database without passing sharding keys, enable the JDBC sharding data source. When it can derive the sharding key from a SQL statement, it routes the statement directly to that shard, except in the cases described after this example:

datasources.default.url=jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=gsmhost)(PORT=1522))(CONNECT_DATA=(SERVICE_NAME=oltp_rw_srvc.orders.oradbcloud)))
datasources.default.connectionFactoryClassName=oracle.jdbc.pool.OracleDataSource
datasources.default.data-source-properties.oracle.jdbc.useShardingDriverConnection=true
datasources:
  default:
    url: jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=gsmhost)(PORT=1522))(CONNECT_DATA=(SERVICE_NAME=oltp_rw_srvc.orders.oradbcloud)))
    connectionFactoryClassName: oracle.jdbc.pool.OracleDataSource
    data-source-properties:
      oracle.jdbc.useShardingDriverConnection: true
[datasources.default]
url = "jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=gsmhost)(PORT=1522))(CONNECT_DATA=(SERVICE_NAME=oltp_rw_srvc.orders.oradbcloud)))"
connectionFactoryClassName = "oracle.jdbc.pool.OracleDataSource"

[datasources.default.data-source-properties]
"oracle.jdbc.useShardingDriverConnection" = true
datasources {
  'default' {
    url = "jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=gsmhost)(PORT=1522))(CONNECT_DATA=(SERVICE_NAME=oltp_rw_srvc.orders.oradbcloud)))"
    connectionFactoryClassName = "oracle.jdbc.pool.OracleDataSource"
    dataSourceProperties {
      oracle.jdbc.useShardingDriverConnection = true
    }
  }
}
{
  datasources {
    default {
      url = "jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=gsmhost)(PORT=1522))(CONNECT_DATA=(SERVICE_NAME=oltp_rw_srvc.orders.oradbcloud)))"
      connectionFactoryClassName = "oracle.jdbc.pool.OracleDataSource"
      data-source-properties {
        "oracle.jdbc.useShardingDriverConnection" = true
      }
    }
  }
}
{
  "datasources": {
    "default": {
      "url": "jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=gsmhost)(PORT=1522))(CONNECT_DATA=(SERVICE_NAME=oltp_rw_srvc.orders.oradbcloud)))",
      "connectionFactoryClassName": "oracle.jdbc.pool.OracleDataSource",
      "data-source-properties": {
        "oracle.jdbc.useShardingDriverConnection": true
      }
    }
  }
}
Oracle documents the sharding data source as a plain OracleDataSource: the driver keeps its own UCP pools for the shard catalog and for the shards, so it needs no pool of its own. Under micronaut-jdbc-ucp, every pooled connection is a sharding connection backed by those internal pools. This combination has not been tested against a sharded database.

The sharding data source works with the JDBC Thin driver only. It runs PL/SQL on the shard catalog, and so does every statement while auto-commit is off, which includes the statements of Micronaut transactions. Setting oracle.jdbc.allowSingleShardTransactionSupport to true runs those transactions on a shard instead, so each transaction must stay within a single shard.

Fast Connection Failover (FCF) lets UCP react to Fast Application Notification (FAN) events from Oracle RAC or Data Guard: it removes connections to a failed instance and rebalances connections when an instance comes back. FCF is off by default. It needs Oracle’s ONS client libraries on the runtime classpath, whose versions are managed by Micronaut:

runtimeOnly("com.oracle.database.ha:ons")
<dependency>
    <groupId>com.oracle.database.ha</groupId>
    <artifactId>ons</artifactId>
    <scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "com.oracle.database.ha:ons",
]

runtimeOnly("com.oracle.database.ha:simplefan")
<dependency>
    <groupId>com.oracle.database.ha</groupId>
    <artifactId>simplefan</artifactId>
    <scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "com.oracle.database.ha:simplefan",
]

datasources.default.fast-connection-failover-enabled=true
datasources.default.ons-configuration=nodes=racnode1:6200,racnode2:6200
datasources:
  default:
    fast-connection-failover-enabled: true
    ons-configuration: nodes=racnode1:6200,racnode2:6200
[datasources.default]
fast-connection-failover-enabled = true
ons-configuration = "nodes=racnode1:6200,racnode2:6200"
datasources {
  'default' {
    fastConnectionFailoverEnabled = true
    onsConfiguration = "nodes=racnode1:6200,racnode2:6200"
  }
}
{
  datasources {
    default {
      fast-connection-failover-enabled = true
      ons-configuration = "nodes=racnode1:6200,racnode2:6200"
    }
  }
}
{
  "datasources": {
    "default": {
      "fast-connection-failover-enabled": true,
      "ons-configuration": "nodes=racnode1:6200,racnode2:6200"
    }
  }
}

Without ons-configuration, UCP reads the ONS configuration from the database (auto-ONS), which is enough for most RAC and Data Guard setups. onsconfiguration, the key of the UCP setter setONSConfiguration, also works.

Without the ons and simplefan libraries, a pool with FCF enabled still opens connections, but UCP background threads fail with NoClassDefFoundError: oracle/ons/Subscriber and no FAN events are received.

To pool XA connections for a transaction manager that enlists them in global transactions, enable xa and set the connection factory class to an XADataSource, such as oracle.jdbc.xa.client.OracleXADataSource:

datasources.default.url=jdbc:oracle:thin:@localhost:1521/FREEPDB1
datasources.default.username=scott
datasources.default.password=tiger
datasources.default.xa=true
datasources.default.connection-factory-class-name=oracle.jdbc.xa.client.OracleXADataSource
datasources:
  default:
    url: jdbc:oracle:thin:@localhost:1521/FREEPDB1
    username: scott
    password: tiger
    xa: true
    connection-factory-class-name: oracle.jdbc.xa.client.OracleXADataSource
[datasources.default]
url = "jdbc:oracle:thin:@localhost:1521/FREEPDB1"
username = "scott"
password = "tiger"
xa = true
connection-factory-class-name = "oracle.jdbc.xa.client.OracleXADataSource"
datasources {
  'default' {
    url = "jdbc:oracle:thin:@localhost:1521/FREEPDB1"
    username = "scott"
    password = "tiger"
    xa = true
    connectionFactoryClassName = "oracle.jdbc.xa.client.OracleXADataSource"
  }
}
{
  datasources {
    default {
      url = "jdbc:oracle:thin:@localhost:1521/FREEPDB1"
      username = "scott"
      password = "tiger"
      xa = true
      connection-factory-class-name = "oracle.jdbc.xa.client.OracleXADataSource"
    }
  }
}
{
  "datasources": {
    "default": {
      "url": "jdbc:oracle:thin:@localhost:1521/FREEPDB1",
      "username": "scott",
      "password": "tiger",
      "xa": true,
      "connection-factory-class-name": "oracle.jdbc.xa.client.OracleXADataSource"
    }
  }
}

The data source is then a UCP PoolXADataSource, and a javax.sql.XADataSource bean of the same name returns XA connections from that pool. The XADataSource bean is not a DataSource, so the pool remains the only DataSource bean of its name. UCP does not hand out plain connections from an XA pool, so the DataSource bean borrows an XA connection and returns its local connection, which takes part in local transactions such as @Transactional methods, and closing that connection returns the XA connection to the pool. The pool’s createConnectionBuilder(), which passes sharding keys and labels, returns connections the same way. These connections wrap UCP’s connection, so they aren’t an OracleConnection, a ValidConnection or a LabelableConnection themselves: use connection.unwrap(…​) rather than a cast to reach those interfaces. Startup fails if xa is enabled and the connection factory class is not an XADataSource.

For a list of other properties able to be configured, simply refer to the implementation that is being used. All setter methods are candidates for configuration.

Connection Pool Javadoc Configuration Pool Documentation

Tomcat

PoolProperties

DatasourceConfiguration

Tomcat Connection Pool

Hikari

HikariConfig

DatasourceConfiguration

Hikari Docs

Apache DBCP

BasicDataSource

DatasourceConfiguration

Apache DBCP Configuration

Oracle UCP

PoolDataSource

DatasourceConfiguration

Oracle UCP

Oracle Session Program Auto-Configuration

Micronaut SQL can automatically set the Oracle v$session.program connection property so your application’s sessions are easily identifiable in Oracle tools (such as v$session).

  • Applied only to Oracle datasources (when dialect: ORACLE is set or the JDBC URL starts with jdbc:oracle).

  • The value is taken from micronaut.application.name.

  • Does not override a user-provided v$session.program.

  • Supported for HikariCP, Tomcat JDBC, Apache DBCP2, and Oracle UCP.

  • Scope limited to v$session.program only (it does not set ClientId/Module/Action).

Configure per datasource:

micronaut.application.name=my-app
datasources.default.url=jdbc:oracle:thin:@localhost:1521/orcl
datasources.default.username=user
datasources.default.password=pass
datasources.default.dialect=ORACLE
datasources.other.url=jdbc:oracle:thin:@localhost:1521/orcl
datasources.other.username=user
datasources.other.password=pass
datasources.other.dialect=ORACLE
datasources.other.oracle.session.enabled=false
micronaut:
  application:
    name: my-app

datasources:
  default:
    url: jdbc:oracle:thin:@localhost:1521/orcl
    username: user
    password: pass
    dialect: ORACLE

  other:
    url: jdbc:oracle:thin:@localhost:1521/orcl
    username: user
    password: pass
    dialect: ORACLE
    oracle:
      session:
        enabled: false
[micronaut.application]
name = "my-app"

[datasources.default]
url = "jdbc:oracle:thin:@localhost:1521/orcl"
username = "user"
password = "pass"
dialect = "ORACLE"

[datasources.other]
url = "jdbc:oracle:thin:@localhost:1521/orcl"
username = "user"
password = "pass"
dialect = "ORACLE"

[datasources.other.oracle.session]
enabled = false
micronaut {
  application {
    name = "my-app"
  }
}
datasources {
  'default' {
    url = "jdbc:oracle:thin:@localhost:1521/orcl"
    username = "user"
    password = "pass"
    dialect = "ORACLE"
  }
  other {
    url = "jdbc:oracle:thin:@localhost:1521/orcl"
    username = "user"
    password = "pass"
    dialect = "ORACLE"
    oracle {
      session {
        enabled = false
      }
    }
  }
}
{
  micronaut {
    application {
      name = "my-app"
    }
  }
  datasources {
    default {
      url = "jdbc:oracle:thin:@localhost:1521/orcl"
      username = "user"
      password = "pass"
      dialect = "ORACLE"
    }
    other {
      url = "jdbc:oracle:thin:@localhost:1521/orcl"
      username = "user"
      password = "pass"
      dialect = "ORACLE"
      oracle {
        session {
          enabled = false
        }
      }
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "my-app"
    }
  },
  "datasources": {
    "default": {
      "url": "jdbc:oracle:thin:@localhost:1521/orcl",
      "username": "user",
      "password": "pass",
      "dialect": "ORACLE"
    },
    "other": {
      "url": "jdbc:oracle:thin:@localhost:1521/orcl",
      "username": "user",
      "password": "pass",
      "dialect": "ORACLE",
      "oracle": {
        "session": {
          "enabled": false
        }
      }
    }
  }
}
1 The default data source auto-sets v$session.program = "my-app"
2 The other data source v$session.program not auto-set because datasources.other.oracle.session.enabled is set to false

Alternatively, this configuration will set "My Program" as session name for the Oracle connection and Micronaut will not try to use application name for it:

micronaut.application.name=my-app
datasources.default.url=jdbc:oracle:thin:@localhost:1521/orcl
datasources.default.username=user
datasources.default.password=pass
datasources.default.dialect=ORACLE
datasources.default.data-source-properties.v$session.program=My Program
micronaut:
  application:
    name: my-app
datasources:
  default:
    url: jdbc:oracle:thin:@localhost:1521/orcl
    username: user
    password: pass
    dialect: ORACLE
    data-source-properties:
      v$session.program: "My Program"
[micronaut.application]
name = "my-app"

[datasources.default]
url = "jdbc:oracle:thin:@localhost:1521/orcl"
username = "user"
password = "pass"
dialect = "ORACLE"

[datasources.default.data-source-properties]
"v$session.program" = "My Program"
micronaut {
  application {
    name = "my-app"
  }
}
datasources {
  'default' {
    url = "jdbc:oracle:thin:@localhost:1521/orcl"
    username = "user"
    password = "pass"
    dialect = "ORACLE"
    dataSourceProperties {
      v$session.program = "My Program"
    }
  }
}
{
  micronaut {
    application {
      name = "my-app"
    }
  }
  datasources {
    default {
      url = "jdbc:oracle:thin:@localhost:1521/orcl"
      username = "user"
      password = "pass"
      dialect = "ORACLE"
      data-source-properties {
        "v$session.program" = "My Program"
      }
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "my-app"
    }
  },
  "datasources": {
    "default": {
      "url": "jdbc:oracle:thin:@localhost:1521/orcl",
      "username": "user",
      "password": "pass",
      "dialect": "ORACLE",
      "data-source-properties": {
        "v$session.program": "My Program"
      }
    }
  }
}

4.3 Configuring Multiple Data Sources

To register more than one data source, simply configure them under different names.

datasources.default=...
datasources.warehouse=...
datasources:
    default:
        ...
    warehouse:
        ...
[datasources]
default = "..."
warehouse = "..."
datasources {
  'default' = "..."
  warehouse = "..."
}
{
  datasources {
    default = "..."
    warehouse = "..."
  }
}
{
  "datasources": {
    "default": "...",
    "warehouse": "..."
  }
}

When injecting DataSource beans, the one with the name "default" will be injected unless the injection is qualified with the configured name. If no configuration is named "default", none of the beans will be primary and thus all injections must be qualified. For example:

import io.micronaut.transaction.annotation.Transactional;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@Singleton
public class InventoryService {

    @Inject
    DataSource dataSource; // (1)

    @Inject
    @Named("warehouse")
    DataSource warehouseDataSource; // (2)

    @Transactional // (3)
    public String defaultUrl() throws SQLException {
        return url(dataSource);
    }

    @Transactional("warehouse") // (4)
    public String warehouseUrl() throws SQLException {
        return url(warehouseDataSource);
    }

    private static String url(DataSource dataSource) throws SQLException {
        try (Connection connection = dataSource.getConnection()) {
            return connection.getMetaData().getURL();
        }
    }
}
from typing import Annotated

from jakarta.inject import Inject, Named, Singleton
from javax.sql import DataSource
from micronaut.transaction.annotation import Transactional

@Singleton
class InventoryService:
    data_source: Annotated[DataSource, Inject]  # (1)
    warehouse_data_source: Annotated[DataSource, Inject, Named("warehouse")]  # (2)

    @Transactional  # (3)
    def default_url(self) -> str:
        return self._url(self.data_source)

    @Transactional("warehouse")  # (4)
    def warehouse_url(self) -> str:
        return self._url(self.warehouse_data_source)

    @staticmethod
    def _url(data_source: DataSource) -> str:
        connection = data_source.getConnection()
        try:
            return connection.getMetaData().getURL()
        finally:
            connection.close()
import io.micronaut.transaction.annotation.Transactional
import jakarta.inject.Inject
import jakarta.inject.Named
import jakarta.inject.Singleton
import javax.sql.DataSource

@Singleton
open class InventoryService {

    @Inject
    lateinit var dataSource: DataSource // (1)

    @Inject
    @Named("warehouse")
    lateinit var warehouseDataSource: DataSource // (2)

    @Transactional // (3)
    open fun defaultUrl(): String = url(dataSource)

    @Transactional("warehouse") // (4)
    open fun warehouseUrl(): String = url(warehouseDataSource)

    private fun url(dataSource: DataSource): String =
        dataSource.connection.use { connection -> connection.metaData.url }
}
import io.micronaut.transaction.annotation.Transactional
import jakarta.inject.Inject
import jakarta.inject.Named
import jakarta.inject.Singleton

import javax.sql.DataSource
import java.sql.Connection

@Singleton
class InventoryService {

    @Inject
    DataSource dataSource // (1)

    @Inject
    @Named("warehouse")
    DataSource warehouseDataSource // (2)

    @Transactional // (3)
    String defaultUrl() {
        url(dataSource)
    }

    @Transactional("warehouse") // (4)
    String warehouseUrl() {
        url(warehouseDataSource)
    }

    private static String url(DataSource dataSource) {
        try (Connection connection = dataSource.connection) {
            return connection.metaData.URL
        }
    }
}
1 The "default" data source will be injected
2 The "warehouse" data source will be injected
3 With Micronaut-managed transactions, connections are obtained inside a transactional method; the "default" transaction manager is used
4 The value of io.micronaut.transaction.annotation.Transactional selects the transaction manager of the "warehouse" data source

4.4 Transaction Management

Spring is not required for transaction management with plain JDBC access or other integrations built on top of a JDBC DataSource. To enable Micronaut-managed transactions for those JDBC-backed integrations, add the following dependency:

implementation("io.micronaut.data:micronaut-data-tx-jdbc")
<dependency>
    <groupId>io.micronaut.data</groupId>
    <artifactId>micronaut-data-tx-jdbc</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.data:micronaut-data-tx-jdbc",
]

This module configures the JDBC transaction manager and includes the Jakarta Transactions API.

If you are using Hibernate/JPA, do not add micronaut-data-tx-jdbc for transaction management. Use the Hibernate transaction module instead:

implementation("io.micronaut.data:micronaut-data-tx-hibernate")
<dependency>
    <groupId>io.micronaut.data</groupId>
    <artifactId>micronaut-data-tx-hibernate</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.data:micronaut-data-tx-hibernate",
]

You can annotate transactional methods with either jakarta.transaction.Transactional or io.micronaut.transaction.annotation.Transactional. For read-only operations, you can also use io.micronaut.transaction.annotation.ReadOnly.

To use jakarta.transaction.Transactional, the Micronaut Data annotation processor, which maps it to Micronaut’s transactional advice, must be on the annotation processor path:

annotationProcessor("io.micronaut.data:micronaut-data-processor")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.data</groupId>
        <artifactId>micronaut-data-processor</artifactId>
    </path>
</annotationProcessorPaths>
[tool.pyronaut.dependencies]
build = [
    "io.micronaut.data:micronaut-data-processor",
]

Using @Transactional with JDBC
import jakarta.inject.Singleton;
import jakarta.transaction.Transactional;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

@Singleton
public class BookRepository {

    private final DataSource dataSource;

    public BookRepository(DataSource dataSource) { // (1)
        this.dataSource = dataSource;
    }

    @Transactional // (2)
    public void saveBook(Book book) throws SQLException {
        try (Connection connection = dataSource.getConnection(); // (3)
             PreparedStatement statement = connection.prepareStatement("INSERT INTO books (title, pages) VALUES (?, ?)")) {
            statement.setString(1, book.title());
            statement.setInt(2, book.pages());
            statement.executeUpdate();
        }
    }
}
Using @Transactional with JDBC
from jakarta.inject import Singleton
from jakarta.transaction import Transactional
from javax.sql import DataSource

from micronaut.docs.jdbc.transactions.Book import Book

@Singleton
class BookRepository:
    def __init__(self, data_source: DataSource):  # (1)
        self.data_source = data_source

    @Transactional  # (2)
    def save_book(self, book: Book) -> None:
        connection = self.data_source.getConnection()  # (3)
        try:
            statement = connection.prepareStatement("INSERT INTO books (title, pages) VALUES (?, ?)")
            statement.setString(1, book.title)
            statement.setInt(2, book.pages)
            statement.executeUpdate()
            statement.close()
        finally:
            connection.close()
Using @Transactional with JDBC
import jakarta.inject.Singleton
import jakarta.transaction.Transactional
import javax.sql.DataSource

@Singleton
open class BookRepository(private val dataSource: DataSource) { // (1)

    @Transactional // (2)
    open fun saveBook(book: Book) {
        dataSource.connection.use { connection -> // (3)
            connection.prepareStatement("INSERT INTO books (title, pages) VALUES (?, ?)").use { statement ->
                statement.setString(1, book.title)
                statement.setInt(2, book.pages)
                statement.executeUpdate()
            }
        }
    }
}
Using @Transactional with JDBC
import jakarta.inject.Singleton
import jakarta.transaction.Transactional

import javax.sql.DataSource
import java.sql.Connection
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.sql.Statement

@Singleton
class BookRepository {

    private final DataSource dataSource

    BookRepository(DataSource dataSource) { // (1)
        this.dataSource = dataSource
    }

    @Transactional // (2)
    void saveBook(Book book) {
        try (Connection connection = dataSource.connection // (3)
             PreparedStatement statement = connection.prepareStatement("INSERT INTO books (title, pages) VALUES (?, ?)")) {
            statement.setString(1, book.title)
            statement.setInt(2, book.pages)
            statement.executeUpdate()
        }
    }
}
1 The DataSource is injected; it is transaction aware
2 The public method is annotated with @Transactional
3 The connection obtained from the injected DataSource inside the transactional method participates in the transaction

If you need Micronaut-specific options such as readOnly, transactionManager, or propagation, use io.micronaut.transaction.annotation.Transactional.

This transaction support works with plain JDBC access and with integrations built on top of a JDBC DataSource, such as Jdbi and JDBC-backed jOOQ.

When using direct JDBC access, obtain connections from the injected DataSource inside a transactional method, or from a method that is invoked within a transaction boundary, so Micronaut can associate the connection with the active transaction.

Spring transaction management remains optional and should only be added if your application specifically needs Spring transaction APIs.

4.5 JDBC Health Checks

Once you have configured a JDBC DataSource the JdbcIndicator is activated resulting in the /health endpoint and CurrentHealthStatus interface resolving the health of the JDBC connection.

See the section on the Health Endpoint for more information.

4.6 Data Source Runtime Password Change

Micronaut SQL supports updating JDBC datasource credentials at runtime without restarting the application.

When a RefreshEvent occurs and a datasource’s username or password property changes, the framework detects the change and propagates the new credentials to the underlying connection pool, then evicts existing connections so new ones are created with the updated credentials.

How it works

  • The application observes datasources.<name>.password and datasources.<name>.username configuration keys

  • On a RefreshEvent, every datasource whose username or password key is in the event is checked: its configured username and password are compared with the last applied ones, and the ones that changed are applied together. The new values are always read from the application configuration, the values in the event are not used, so the event should not contain secrets.

  • On a RefreshEvent without specific keys (new RefreshEvent()), every configured datasource is checked the same way. Call Environment.refresh() before publishing such an event so the new values are loaded.

  • Implementations for the supported pools set the new username and/or password on the pool and evict existing connections so subsequent connections use the updated credentials (see the notes below for details per pool).

By default, runtime credential changes are handled automatically for each datasource.

Example configuration:

datasources.default.url=jdbc:postgresql://localhost:5432/app
datasources.default.driver-class-name=org.postgresql.Driver
datasources.default.username=app_user
datasources.default.password=${DB_PASSWORD}
datasources:
  default:
    url: jdbc:postgresql://localhost:5432/app
    driver-class-name: org.postgresql.Driver
    username: app_user
    password: ${DB_PASSWORD}
[datasources.default]
url = "jdbc:postgresql://localhost:5432/app"
driver-class-name = "org.postgresql.Driver"
username = "app_user"
password = "${DB_PASSWORD}"
datasources {
  'default' {
    url = "jdbc:postgresql://localhost:5432/app"
    driverClassName = "org.postgresql.Driver"
    username = "app_user"
    password = "${DB_PASSWORD}"
  }
}
{
  datasources {
    default {
      url = "jdbc:postgresql://localhost:5432/app"
      driver-class-name = "org.postgresql.Driver"
      username = "app_user"
      password = "${DB_PASSWORD}"
    }
  }
}
{
  "datasources": {
    "default": {
      "url": "jdbc:postgresql://localhost:5432/app",
      "driver-class-name": "org.postgresql.Driver",
      "username": "app_user",
      "password": "${DB_PASSWORD}"
    }
  }
}

When DB_PASSWORD is rotated and your configuration refreshes, the datasource password is updated and the pool evicts existing connections so new ones authenticate with the new password.

Example of datasource password rotation implementation:

import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Value;
import io.micronaut.core.util.StringUtils;
import io.micronaut.runtime.context.scope.refresh.RefreshEvent;
import io.micronaut.scheduling.annotation.Scheduled;
import jakarta.inject.Singleton;

import java.util.Map;

@Singleton
final class DbPasswordRefresher {

    private final ApplicationContext applicationContext;
    private final DbSecretStore secretStore;
    private String currentPassword;

    public DbPasswordRefresher(@Value("${datasources.default.password}") String currentPassword,
                               ApplicationContext applicationContext,
                               DbSecretStore secretStore) {
        this.currentPassword = currentPassword;
        this.applicationContext = applicationContext;
        this.secretStore = secretStore;
    }

    @Scheduled(cron = "0 * * * * *") // Runs every minute
    void refresh() {
        String password = getSecretDbPassword(); // Read from Vault, Secret Service, etc.
        if (StringUtils.isNotEmpty(password) && !password.equals(currentPassword)) {
            // This refresh() call is required before publishing event since datasources.default.password
            // needs to be refreshed in the application configuration
            applicationContext.getEnvironment().refresh();
            // publishEvent with such RefreshEvent will trigger connection pool update and old connections eviction
            // The datasource event handler for this event will get actual password from the
            // application configuration that has been refreshed in refresh() call above
            // and sending such event without prior calling refresh() will not work properly.
            // Only the changed key matters, the event value is not used: do not put the secret into the event.
            applicationContext.publishEvent(new RefreshEvent(Map.of("datasources.default.password", "<redacted>")));
            this.currentPassword = password;
        }
    }

    private String getSecretDbPassword() {
        return secretStore.currentPassword();
    }
}
from typing import Annotated

from jakarta.inject import Singleton
from micronaut.context import ApplicationContext
from micronaut.context.annotation import Value
from micronaut.runtime.context.scope.refresh import RefreshEvent
from micronaut.scheduling.annotation import Scheduled

from micronaut.docs.jdbc.refresh.DbSecretStore import DbSecretStore

@Singleton
class DbPasswordRefresher:
    def __init__(self,
                 current_password: Annotated[str, Value("${datasources.default.password}")],
                 application_context: ApplicationContext,
                 secret_store: DbSecretStore):
        self.current_password = current_password
        self.application_context = application_context
        self.secret_store = secret_store

    @Scheduled(cron="0 * * * * *")  # Runs every minute
    def refresh(self) -> None:
        password = self.get_secret_db_password()  # Read from Vault, Secret Service, etc.
        if password and password != self.current_password:
            # This refresh() call is required before publishing event since datasources.default.password
            # needs to be refreshed in the application configuration
            self.application_context.getEnvironment().refresh()
            # publishEvent with such RefreshEvent will trigger connection pool update and old connections eviction
            # The datasource event handler for this event will get actual password from the
            # application configuration that has been refreshed in refresh() call above
            # and sending such event without prior calling refresh() will not work properly.
            # Only the changed key matters, the event value is not used: do not put the secret into the event.
            self.application_context.publishEvent(RefreshEvent({"datasources.default.password": "<redacted>"}))
            self.current_password = password

    def get_secret_db_password(self) -> str | None:
        return self.secret_store.current_password()
import io.micronaut.context.ApplicationContext
import io.micronaut.context.annotation.Value
import io.micronaut.runtime.context.scope.refresh.RefreshEvent
import io.micronaut.scheduling.annotation.Scheduled
import jakarta.inject.Singleton

@Singleton
open class DbPasswordRefresher(
    @Value("\${datasources.default.password}") private var currentPassword: String,
    private val applicationContext: ApplicationContext,
    private val secretStore: DbSecretStore
) {

    @Scheduled(cron = "0 * * * * *") // Runs every minute
    open fun refresh() {
        val password = getSecretDbPassword() // Read from Vault, Secret Service, etc.
        if (!password.isNullOrEmpty() && password != currentPassword) {
            // This refresh() call is required before publishing event since datasources.default.password
            // needs to be refreshed in the application configuration
            applicationContext.environment.refresh()
            // publishEvent with such RefreshEvent will trigger connection pool update and old connections eviction
            // The datasource event handler for this event will get actual password from the
            // application configuration that has been refreshed in refresh() call above
            // and sending such event without prior calling refresh() will not work properly.
            // Only the changed key matters, the event value is not used: do not put the secret into the event.
            applicationContext.publishEvent(RefreshEvent(mapOf<String, Any>("datasources.default.password" to "<redacted>")))
            currentPassword = password
        }
    }

    private fun getSecretDbPassword(): String? = secretStore.currentPassword()
}
import io.micronaut.context.ApplicationContext
import io.micronaut.context.annotation.Value
import io.micronaut.runtime.context.scope.refresh.RefreshEvent
import io.micronaut.scheduling.annotation.Scheduled
import jakarta.inject.Singleton

@Singleton
class DbPasswordRefresher {

    private final ApplicationContext applicationContext
    private final DbSecretStore secretStore
    private String currentPassword

    DbPasswordRefresher(@Value('${datasources.default.password}') String currentPassword,
                        ApplicationContext applicationContext,
                        DbSecretStore secretStore) {
        this.currentPassword = currentPassword
        this.applicationContext = applicationContext
        this.secretStore = secretStore
    }

    @Scheduled(cron = "0 * * * * *") // Runs every minute
    void refresh() {
        String password = getSecretDbPassword() // Read from Vault, Secret Service, etc.
        if (password && password != currentPassword) {
            // This refresh() call is required before publishing event since datasources.default.password
            // needs to be refreshed in the application configuration
            applicationContext.environment.refresh()
            // publishEvent with such RefreshEvent will trigger connection pool update and old connections eviction
            // The datasource event handler for this event will get actual password from the
            // application configuration that has been refreshed in refresh() call above
            // and sending such event without prior calling refresh() will not work properly.
            // Only the changed key matters, the event value is not used: do not put the secret into the event.
            applicationContext.publishEvent(new RefreshEvent(["datasources.default.password": "<redacted>"]))
            this.currentPassword = password
        }
    }

    private String getSecretDbPassword() {
        secretStore.currentPassword()
    }
}

Alternatively, publish the keys reported by Environment.refreshAndDiff(). They include the datasources.<name>.password key when the placeholder it references changes. The reported values are the previous values, which may be secrets, so replace them before publishing the event:

import io.micronaut.context.ApplicationContext;
import io.micronaut.runtime.context.scope.refresh.RefreshEvent;
import io.micronaut.scheduling.annotation.Scheduled;
import jakarta.inject.Singleton;

import java.util.HashMap;
import java.util.Map;

@Singleton
final class DbPasswordRefresher {

    private final ApplicationContext applicationContext;

    public DbPasswordRefresher(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Scheduled(cron = "0 * * * * *")
    void refresh() {
        // refreshAndDiff() reloads config values (Vault, Secret, etc.), updates the application configuration
        // and returns the changed keys. When ${DB_PASSWORD} changes, the properties that reference it,
        // such as datasources.default.password, are reported as changed too.
        Map<String, Object> changes = applicationContext.getEnvironment().refreshAndDiff();
        if (!changes.isEmpty()) {
            // The values in the changes are the previous values, which may be secrets, and refresh event
            // listeners only need the changed keys, so publish the keys with redacted values.
            // The datasource event handler for this event will get actual password from the
            // application configuration that has been refreshed in refreshAndDiff() call above
            Map<String, Object> changedKeys = new HashMap<>(changes.size());
            changes.keySet().forEach(key -> changedKeys.put(key, "<redacted>"));
            applicationContext.publishEvent(new RefreshEvent(changedKeys));
        }
    }
}
from jakarta.inject import Singleton
from micronaut.context import ApplicationContext
from micronaut.runtime.context.scope.refresh import RefreshEvent
from micronaut.scheduling.annotation import Scheduled

@Singleton
class DbPasswordRefresher:
    def __init__(self, application_context: ApplicationContext):
        self.application_context = application_context

    @Scheduled(cron="0 * * * * *")
    def refresh(self) -> None:
        # refreshAndDiff() reloads config values (Vault, Secret, etc.), updates the application configuration
        # and returns the changed keys. When ${DB_PASSWORD} changes, the properties that reference it,
        # such as datasources.default.password, are reported as changed too.
        changes = self.application_context.getEnvironment().refreshAndDiff()
        if not changes.isEmpty():
            # The values in the changes are the previous values, which may be secrets, and refresh event
            # listeners only need the changed keys, so publish the keys with redacted values.
            # The datasource event handler for this event will get actual password from the
            # application configuration that has been refreshed in refreshAndDiff() call above
            changed_keys = {key: "<redacted>" for key in changes.keySet()}
            self.application_context.publishEvent(RefreshEvent(changed_keys))
import io.micronaut.context.ApplicationContext
import io.micronaut.runtime.context.scope.refresh.RefreshEvent
import io.micronaut.scheduling.annotation.Scheduled
import jakarta.inject.Singleton

@Singleton
open class DbPasswordRefresher(private val applicationContext: ApplicationContext) {

    @Scheduled(cron = "0 * * * * *")
    open fun refresh() {
        // refreshAndDiff() reloads config values (Vault, Secret, etc.), updates the application configuration
        // and returns the changed keys. When ${DB_PASSWORD} changes, the properties that reference it,
        // such as datasources.default.password, are reported as changed too.
        val changes = applicationContext.environment.refreshAndDiff()
        if (changes.isNotEmpty()) {
            // The values in the changes are the previous values, which may be secrets, and refresh event
            // listeners only need the changed keys, so publish the keys with redacted values.
            // The datasource event handler for this event will get actual password from the
            // application configuration that has been refreshed in refreshAndDiff() call above
            val changedKeys = changes.keys.associateWith<String, Any> { "<redacted>" }
            applicationContext.publishEvent(RefreshEvent(changedKeys))
        }
    }
}
import io.micronaut.context.ApplicationContext
import io.micronaut.runtime.context.scope.refresh.RefreshEvent
import io.micronaut.scheduling.annotation.Scheduled
import jakarta.inject.Singleton

@Singleton
class DbPasswordRefresher {

    private final ApplicationContext applicationContext

    DbPasswordRefresher(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext
    }

    @Scheduled(cron = "0 * * * * *")
    void refresh() {
        // refreshAndDiff() reloads config values (Vault, Secret, etc.), updates the application configuration
        // and returns the changed keys. When ${DB_PASSWORD} changes, the properties that reference it,
        // such as datasources.default.password, are reported as changed too.
        Map<String, Object> changes = applicationContext.environment.refreshAndDiff()
        if (changes) {
            // The values in the changes are the previous values, which may be secrets, and refresh event
            // listeners only need the changed keys, so publish the keys with redacted values.
            // The datasource event handler for this event will get actual password from the
            // application configuration that has been refreshed in refreshAndDiff() call above
            Map<String, Object> changedKeys = changes.collectEntries { key, value -> [(key): "<redacted>"] }
            applicationContext.publishEvent(new RefreshEvent(changedKeys))
        }
    }
}

Notes

  • This mechanism is event-driven. Any mechanism that results in a Micronaut RefreshEvent with changed datasources.* username/password properties will be handled.

  • Username changes are supported as well; if both username and password change in the same refresh cycle, both are applied.

  • How each pool applies the new credentials:

    • HikariCP and Tomcat JDBC: existing connections are softly evicted, idle connections are closed immediately and connections in use are closed when they are returned to the pool.

    • Oracle UCP: the pool is reconfigured with PoolDataSource.reconfigureDataSource.

    • Apache DBCP2: it does not apply credential changes to a running pool, so the pool is restarted instead. Idle connections are closed immediately, connections in use are closed when they are returned to the pool. When the datasource is wrapped by a BeanCreatedEventListener, for example by Micronaut Data JDBC, Micronaut Core 5.2.10 or later is required.

    • Unpooled: the next connection uses the new credentials.

  • Only the datasources.<name>.username and datasources.<name>.password properties are observed. Credentials set through other properties, such as data-source-properties or connection-properties, are not updated at runtime.

  • Credentials of disabled datasources (datasources.<name>.enabled: false) are not refreshed. Datasources are created at startup, so enabling or disabling a datasource requires an application restart.

  • The new credentials are applied as they are, without being verified first. Update the credentials in the database, or make sure it accepts both the old and the new ones, before publishing the event. Otherwise evicted connections can not be re-established.

  • If a username is changed to empty, or a username or password can not be read from the configuration, the credentials refresh ignores the whole change of that datasource and logs a warning, so it never applies a new username with an old password or the other way round. Note that Tomcat JDBC, Oracle UCP and Apache DBCP2 use the datasource configuration that Micronaut re-binds on a refresh event as the pool configuration, so for these pools a changed value can still reach the pool configuration directly.

  • A failure to update the pool of one datasource is logged as an error and does not prevent other datasources from being updated.

  • A skipped or failed change is not lost: it is applied by the next refresh event that contains a username or password key of that datasource, or by new RefreshEvent(). new RefreshEvent() does not change anything when no credentials change is pending, so it can be published periodically to retry.

  • Supported pools: HikariCP, Tomcat JDBC, Oracle UCP, Apache DBCP2 and unpooled datasources.

4.7 Oracle JDBC Driver Extensions

Starting with Oracle AI Database Release 26ai, you can extend the capabilities of Oracle JDBC drivers through service providers.

Oracle JDBC Driver Extensions are service providers that the Oracle JDBC driver loads from the classpath. There are two kinds:

  • Configuration providers supply a whole connection configuration, including the connect descriptor, user and password, from a file, an HTTPS server, OCI Object Storage, OCI Database Tools, Azure App Configuration and other sources. The data source URL names the provider.

  • Resource providers supply one value, such as a password, an access token, a connection string, a TLS configuration or a trace event listener. They are configured with connection properties.

Micronaut SQL manages the versions of the provider artifacts, so you can add them without a version:

implementation("com.oracle.database.jdbc:ojdbc-provider-oci")
<dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc-provider-oci</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "com.oracle.database.jdbc:ojdbc-provider-oci",
]

The managed artifacts are ojdbc-provider-common, ojdbc-provider-oci, ojdbc-provider-azure, ojdbc-provider-aws, ojdbc-provider-gcp, ojdbc-provider-hashicorp, ojdbc-provider-jackson-oson and ojdbc-provider-observability. The providers depend on the Oracle wallet library, com.oracle.database.security:oraclepki, which is managed at the version of the driver.

Configuration providers

With a configuration provider, the data source needs only a URL of the form jdbc:oracle:thin:@config-<provider>://<location>. The built-in file provider reads a JSON file:

datasources.default.url=jdbc:oracle:thin:@config-file:///etc/myapp/oracle-config.json
datasources:
  default:
    url: jdbc:oracle:thin:@config-file:///etc/myapp/oracle-config.json
[datasources.default]
url = "jdbc:oracle:thin:@config-file:///etc/myapp/oracle-config.json"
datasources {
  'default' {
    url = "jdbc:oracle:thin:@config-file:///etc/myapp/oracle-config.json"
  }
}
{
  datasources {
    default {
      url = "jdbc:oracle:thin:@config-file:///etc/myapp/oracle-config.json"
    }
  }
}
{
  "datasources": {
    "default": {
      "url": "jdbc:oracle:thin:@config-file:///etc/myapp/oracle-config.json"
    }
  }
}
{
  "connect_descriptor": "dbhost:1521/orders",
  "user": "app",
  "password": {"type": "base64", "value": "c2VjcmV0"}
}

Other providers follow the same pattern, for example jdbc:oracle:thin:@config-ociobject://mytenancy.objectstorage.us-phoenix-1.oci.customer-oci.com/n/mytenancy/b/bucket1/o/payload.json or jdbc:oracle:thin:@config-azure://myappconfig?key=/sales_app1/&label=dev. See the Oracle JDBC Driver Extensions documentation for the options of each provider.

Resource providers

Resource providers are configured via connection properties. Use the datasources.*.data-source-properties prefix.

For example, to configure the oracle.jdbc.provider.password property as the name of a password provider and oracle.jdbc.provider.password.vaultId as a parameter recognized by the password provider, you will use a configuration such as:

datasources.default.data-source-properties.oracle.jdbc.provider.password=example-provider
datasources.default.data-source-properties.oracle.jdbc.provider.password.vaultId=9999-8888-7777

The same applies to the other resource provider properties: oracle.jdbc.provider.accessToken for OCI IAM or Azure AD database tokens, oracle.jdbc.provider.connectionString, oracle.jdbc.provider.tlsConfiguration, and oracle.jdbc.provider.traceEventListener for the tracing providers of ojdbc-provider-observability.

With Oracle UCP, settings that take Java objects rather than values, such as a token supplier, are configured with a PoolDataSourceCustomizer instead.

4.8 SQLite Support

To use SQLite, add the following dependency:

runtimeOnly("io.micronaut.sql:micronaut-jdbc-sqlite")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jdbc-sqlite</artifactId>
    <scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-jdbc-sqlite",
]

The module provides connection capabilities configuration for SQLite and adds the SQLite JDBC driver as a transitive dependency.

If you do not provide an explicit JDBC URL, Micronaut SQL configures automatically the following JDBC URL jdbc:sqlite:file:%s?mode=memory&cache=shared&foreign_keys=on&busy_timeout=5000, when you set datasources.*.db-type to sqlite.

For production, you will probably want to configure SQLite explicitly with a file-backed JDBC URL and something like this:

datasources.default.url=jdbc:sqlite:mydb.db?foreign_keys=on&busy_timeout=5000&journal_mode=WAL&synchronous
datasources:
    default:
        url: jdbc:sqlite:mydb.db?foreign_keys=on&busy_timeout=5000&journal_mode=WAL&synchronous
[datasources.default]
url = "jdbc:sqlite:mydb.db?foreign_keys=on&busy_timeout=5000&journal_mode=WAL&synchronous"
datasources {
  'default' {
    url = "jdbc:sqlite:mydb.db?foreign_keys=on&busy_timeout=5000&journal_mode=WAL&synchronous"
  }
}
{
  datasources {
    default {
      url = "jdbc:sqlite:mydb.db?foreign_keys=on&busy_timeout=5000&journal_mode=WAL&synchronous"
    }
  }
}
{
  "datasources": {
    "default": {
      "url": "jdbc:sqlite:mydb.db?foreign_keys=on&busy_timeout=5000&journal_mode=WAL&synchronous"
    }
  }
}

Useful SQLite documentation links:

5 Configuring Hibernate

Setting up a Hibernate/JPA EntityManager

Using the CLI

If you are creating your project using the Micronaut CLI, supply the hibernate-jpa feature to include a Hibernate JPA configuration in your project:

$ mn create-app my-app --features hibernate-jpa

Micronaut features built in support for configuring a Hibernate / JPA EntityManager that builds on the SQL DataSource support.

Once you have configured one or many DataSources to use Hibernate, you will need to add the hibernate-jpa dependency to your build configuration:

implementation("io.micronaut.sql:micronaut-hibernate-jpa")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-hibernate-jpa</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-hibernate-jpa",
]

and the Micronaut Data Transaction Hibernate dependency:

implementation("io.micronaut.data:micronaut-data-tx-hibernate")
<dependency>
    <groupId>io.micronaut.data</groupId>
    <artifactId>micronaut-data-tx-hibernate</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.data:micronaut-data-tx-hibernate",
]

And that is it. For each registered SQL DataSource, Micronaut will configure the following beans using EntityManagerFactoryBean:

5.1 Disable Micronaut Hibernate JPA

You can disable Micronaut Hibernate JPA, for example in a test, by setting jpa.enabled to false.

To disable a single JPA configuration and keep the others, set jpa.<name>.enabled to false, for example jpa.default.enabled. No SessionFactory is created for a disabled configuration, so Hibernate does not connect to its database at startup.

5.2 Injecting an EntityManager or Hibernate Session

You can use the jakarta.persistence.PersistenceContext annotation to inject an EntityManager (or Hibernate Session):

Using @PersistenceContext
import io.micronaut.transaction.annotation.Transactional;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;

@Singleton
public class BookRepository {

    @PersistenceContext
    EntityManager entityManager; // (1)

    @PersistenceContext(name = "other")
    EntityManager otherManager; // (2)

    @Transactional // (3)
    public Book save(String title) {
        Book book = new Book(title);
        entityManager.persist(book);
        return book;
    }

    @Transactional("other") // (4)
    public Book saveToOther(String title) {
        Book book = new Book(title);
        otherManager.persist(book);
        return book;
    }

    @Transactional(readOnly = true)
    public Book findById(Long id) {
        return entityManager.find(Book.class, id);
    }

    @Transactional(value = "other", readOnly = true)
    public Book findInOtherById(Long id) {
        return otherManager.find(Book.class, id);
    }
}
Using @PersistenceContext
import io.micronaut.transaction.annotation.Transactional
import jakarta.inject.Singleton
import jakarta.persistence.EntityManager
import jakarta.persistence.PersistenceContext

@Singleton
open class BookRepository {

    @PersistenceContext
    lateinit var entityManager: EntityManager // (1)

    @PersistenceContext(name = "other")
    lateinit var otherManager: EntityManager // (2)

    @Transactional // (3)
    open fun save(title: String): Book {
        val book = Book(title)
        entityManager.persist(book)
        return book
    }

    @Transactional("other") // (4)
    open fun saveToOther(title: String): Book {
        val book = Book(title)
        otherManager.persist(book)
        return book
    }

    @Transactional(readOnly = true)
    open fun findById(id: Long): Book? = entityManager.find(Book::class.java, id)

    @Transactional(value = "other", readOnly = true)
    open fun findInOtherById(id: Long): Book? = otherManager.find(Book::class.java, id)
}
Using @PersistenceContext
import io.micronaut.transaction.annotation.Transactional
import jakarta.inject.Singleton
import jakarta.persistence.EntityManager
import jakarta.persistence.PersistenceContext

@Singleton
class BookRepository {

    @PersistenceContext
    EntityManager entityManager // (1)

    @PersistenceContext(name = "other")
    EntityManager otherManager // (2)

    @Transactional // (3)
    Book save(String title) {
        Book book = new Book(title)
        entityManager.persist(book)
        return book
    }

    @Transactional("other") // (4)
    Book saveToOther(String title) {
        Book book = new Book(title)
        otherManager.persist(book)
        return book
    }

    @Transactional(readOnly = true)
    Book findById(Long id) {
        entityManager.find(Book, id)
    }

    @Transactional(value = "other", readOnly = true)
    Book findInOtherById(Long id) {
        otherManager.find(Book, id)
    }
}
1 The EntityManager of the default data source is injected
2 The name member selects the EntityManager of another configured data source (other here)
3 Micronaut will inject a compile time scoped proxy that retrieves the EntityManager associated with the current transaction when using jakarta.transaction.Transactional (or io.micronaut.transaction.annotation.Transactional)
4 When using several data sources, the value of io.micronaut.transaction.annotation.Transactional selects the transaction manager of the data source the EntityManager belongs to

For Hibernate/JPA transaction management, use micronaut-data-tx-hibernate as described in Setting up a Hibernate/JPA EntityManager.

5.3 Customizing Hibernate/JPA Configuration

There are several different ways you can customize and configure how the SessionFactory is built. The easiest way is via configuration. The following configuration demonstrates an example:

Configuring Hibernate Properties
datasources.default.name=mydb
jpa.default.entity-scan.packages[0]=foo.bar
jpa.default.entity-scan.packages[1]=foo.baz
jpa.default.properties.hibernate.hbm2ddl.auto=update
jpa.default.properties.hibernate.show_sql=true
datasources:
    default:
        name: 'mydb'
jpa:
    default:
        entity-scan:
            packages:
                - 'foo.bar'
                - 'foo.baz'
        properties:
            hibernate:
                hbm2ddl:
                    auto: update
                show_sql: true
[datasources.default]
name = "mydb"

[jpa.default.entity-scan]
packages = ["foo.bar", "foo.baz"]

[jpa.default.properties.hibernate]
show_sql = true

[jpa.default.properties.hibernate.hbm2ddl]
auto = "update"
datasources {
  'default' {
    name = "mydb"
  }
}
jpa {
  'default' {
    entityScan {
      packages = ["foo.bar", "foo.baz"]
    }
    properties {
      hibernate {
        hbm2ddl {
          auto = "update"
        }
        show_sql = true
      }
    }
  }
}
{
  datasources {
    default {
      name = "mydb"
    }
  }
  jpa {
    default {
      entity-scan {
        packages = ["foo.bar", "foo.baz"]
      }
      properties {
        hibernate {
          hbm2ddl {
            auto = "update"
          }
          show_sql = true
        }
      }
    }
  }
}
{
  "datasources": {
    "default": {
      "name": "mydb"
    }
  },
  "jpa": {
    "default": {
      "entity-scan": {
        "packages": ["foo.bar", "foo.baz"]
      },
      "properties": {
        "hibernate": {
          "hbm2ddl": {
            "auto": "update"
          },
          "show_sql": true
        }
      }
    }
  }
}

The above example configures the packages to be scanned and sets properties to be passed to Hibernate. As you can see these are done on a per DataSource basis. Refer to the JpaConfiguration configuration class for the possible options.

If you need even further control over how the SessionFactory is built then you can register BeanCreatedEventListener beans that listen for the creation of the SessionFactoryBuilder, MetadataSources etc. and apply your custom configuration in the listener.

You may also optionally create beans of type Integrator and Interceptor and these will be picked up and injected automatically.

5.4 Entity Scan Configuration

Since 1.2 of this library Entity scan configuration is more flexible and it is possible to do reflection free scanning that works on GraalVM substrate.

The default configuration will look for all classes compiled by Micronaut that include the @Entity annotation.

If you wish to limit the packages to include in a particular JPA entity manager you can do so with the entity-scan configuration option:

Limiting Entity Scan
jpa.default.entity-scan.packages[0]=foo.bar
jpa:
    default:
        entity-scan:
            packages:
                - 'foo.bar'
[jpa.default.entity-scan]
packages = ["foo.bar"]
jpa {
  'default' {
    entityScan {
      packages = ["foo.bar"]
    }
  }
}
{
  jpa {
    default {
      entity-scan {
        packages = ["foo.bar"]
      }
    }
  }
}
{
  "jpa": {
    "default": {
      "entity-scan": {
        "packages": ["foo.bar"]
      }
    }
  }
}

The above configuration limits the search to only classes in the foo.bar package. Note that if classes are not compiled by Micronaut they will not be found. There are two ways to resolve this, one is to generate introspection metadata for the external classes. For example you can place in this on your Application class:

Generating Introspection Metadata for External Classes
import io.micronaut.core.annotation.Introspected;
import jakarta.persistence.Entity;

@Introspected(packages = "io.micronaut.docs.hibernate.entityscan.external", includedAnnotations = Entity.class) // (1)
public class Application {
}
Generating Introspection Metadata for External Classes
import io.micronaut.core.annotation.Introspected
import jakarta.persistence.Entity

@Introspected(packages = ["io.micronaut.docs.hibernate.entityscan.external"], includedAnnotations = [Entity::class]) // (1)
class Application
Generating Introspection Metadata for External Classes
import io.micronaut.core.annotation.Introspected
import jakarta.persistence.Entity

@Introspected(packages = "io.micronaut.docs.hibernate.entityscan.external", includedAnnotations = Entity) // (1)
class Application {
}
1 This will generate introspection metadata for all the @Entity classes in the given package. The includedAnnotations member is required when packages is specified.

If this option doesn’t work for you, you can instead enable full classpath scanning using the classpath property:

Enabling Full Classpath Scanning
jpa.default.entity-scan.classpath=true
jpa.default.entity-scan.packages[0]=foo.bar
jpa:
    default:
        entity-scan:
            classpath: true
            packages:
                - 'foo.bar'
[jpa.default.entity-scan]
classpath = true
packages = ["foo.bar"]
jpa {
  'default' {
    entityScan {
      classpath = true
      packages = ["foo.bar"]
    }
  }
}
{
  jpa {
    default {
      entity-scan {
        classpath = true
        packages = ["foo.bar"]
      }
    }
  }
}
{
  "jpa": {
    "default": {
      "entity-scan": {
        "classpath": true,
        "packages": ["foo.bar"]
      }
    }
  }
}

Note that this approach has the following disadvantages:

  • It is slower, since Micronaut has to search through JAR files and scan class files with ASM

  • It does not work in GraalVM substrate.

5.5 GraalVM native image

To run Hibernate on GraalVM native image you need to configure more than entity scanning alone. The Hibernate runtime normally depends on reflective model inspection and runtime proxy generation, both of which need extra setup in a native image.

For Hibernate applications on GraalVM, make sure all of the following are true:

  • Entity discovery uses Micronaut entity scanning, not full classpath scanning

  • Lazy associations use compile-time Hibernate proxies instead of runtime-generated proxies

  • Types that Hibernate still needs to inspect reflectively are registered for reflection

Entity Scanning

Micronaut’s default entity scanning works in native image because it relies on Micronaut metadata instead of scanning the full classpath.

Avoid enabling jpa.*.entity-scan.classpath=true for GraalVM native image. Full classpath scanning does not work in a native executable.

If your entities are not compiled by Micronaut, generate introspection metadata for them instead:

import io.micronaut.core.annotation.Introspected;
import jakarta.persistence.Entity;

@Introspected(packages = "io.micronaut.docs.hibernate.entityscan.external", includedAnnotations = Entity.class) // (1)
public class Application {
}
import io.micronaut.core.annotation.Introspected
import jakarta.persistence.Entity

@Introspected(packages = ["io.micronaut.docs.hibernate.entityscan.external"], includedAnnotations = [Entity::class]) // (1)
class Application
import io.micronaut.core.annotation.Introspected
import jakarta.persistence.Entity

@Introspected(packages = "io.micronaut.docs.hibernate.entityscan.external", includedAnnotations = Entity) // (1)
class Application {
}
1 Generates introspection metadata for the @Entity classes of the given package

See the entity scan configuration section for the full set of options.

Lazy Associations

If your model uses lazy associations, Hibernate must create proxies for the associated entity types. Runtime proxy generation is not a good fit for GraalVM native image, so use Micronaut’s compile-time Hibernate proxies instead.

Enable Compile-Time Hibernate Proxies
jpa.default.compile-time-hibernate-proxies=true
jpa:
  default:
    compile-time-hibernate-proxies: true
[jpa.default]
compile-time-hibernate-proxies = true
jpa {
  'default' {
    compileTimeHibernateProxies = true
  }
}
{
  jpa {
    default {
      compile-time-hibernate-proxies = true
    }
  }
}
{
  "jpa": {
    "default": {
      "compile-time-hibernate-proxies": true
    }
  }
}

Each entity type that Hibernate may proxy must be annotated with @GenerateProxy:

import io.micronaut.configuration.hibernate.jpa.proxy.GenerateProxy;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
@GenerateProxy
public class Owner {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
import io.micronaut.configuration.hibernate.jpa.proxy.GenerateProxy
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id

@Entity
@GenerateProxy
open class Owner {

    @Id
    @GeneratedValue
    open var id: Long? = null

    open var name: String? = null
}
import io.micronaut.configuration.hibernate.jpa.proxy.GenerateProxy
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id

@Entity
@GenerateProxy
class Owner {

    @Id
    @GeneratedValue
    Long id

    String name
}

Then any lazy association pointing to that entity can be resolved without falling back to Hibernate’s runtime proxy generation:

import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;

@Entity
public class Pet {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    private Owner owner;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Owner getOwner() {
        return owner;
    }

    public void setOwner(Owner owner) {
        this.owner = owner;
    }
}
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id
import jakarta.persistence.ManyToOne

@Entity
open class Pet {

    @Id
    @GeneratedValue
    open var id: Long? = null

    open var name: String? = null

    @ManyToOne(fetch = FetchType.LAZY)
    open var owner: Owner? = null
}
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id
import jakarta.persistence.ManyToOne

@Entity
class Pet {

    @Id
    @GeneratedValue
    Long id

    String name

    @ManyToOne(fetch = FetchType.LAZY)
    Owner owner
}
For GraalVM native image, enable compile-time Hibernate proxies as shown above. The @GenerateProxy annotation is still required on each entity type that Hibernate may proxy.

Reflection Requirements

Even with Micronaut entity scanning and compile-time proxies enabled, Hibernate may still need reflective access to some model types at runtime.

In particular, embeddable identifier types used with @EmbeddedId should be annotated with @ReflectiveAccess:

import io.micronaut.core.annotation.ReflectiveAccess;
import jakarta.persistence.Embeddable;

import java.io.Serializable;
import java.util.Objects;

@Embeddable
@ReflectiveAccess
public class OrderId implements Serializable {

    private String region;
    private Long number;

    public OrderId() {
    }

    public OrderId(String region, Long number) {
        this.region = region;
        this.number = number;
    }

    public String getRegion() {
        return region;
    }

    public void setRegion(String region) {
        this.region = region;
    }

    public Long getNumber() {
        return number;
    }

    public void setNumber(Long number) {
        this.number = number;
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof OrderId other && Objects.equals(region, other.region) && Objects.equals(number, other.number);
    }

    @Override
    public int hashCode() {
        return Objects.hash(region, number);
    }
}
import io.micronaut.core.annotation.ReflectiveAccess
import jakarta.persistence.Embeddable
import java.io.Serializable

@Embeddable
@ReflectiveAccess
data class OrderId(var region: String? = null, var number: Long? = null) : Serializable
import groovy.transform.EqualsAndHashCode
import io.micronaut.core.annotation.ReflectiveAccess
import jakarta.persistence.Embeddable

@Embeddable
@ReflectiveAccess
@EqualsAndHashCode
class OrderId implements Serializable {

    String region
    Long number

    OrderId() {
    }

    OrderId(String region, Long number) {
        this.region = region
        this.number = number
    }
}

And then used from the entity as usual:

import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

@Entity
@Table(name = "orders")
public class Order {

    @EmbeddedId
    private OrderId id;

    private String customer;

    public OrderId getId() {
        return id;
    }

    public void setId(OrderId id) {
        this.id = id;
    }

    public String getCustomer() {
        return customer;
    }

    public void setCustomer(String customer) {
        this.customer = customer;
    }
}
import jakarta.persistence.EmbeddedId
import jakarta.persistence.Entity
import jakarta.persistence.Table

@Entity
@Table(name = "orders")
open class Order {

    @EmbeddedId
    open var id: OrderId? = null

    open var customer: String? = null
}
import jakarta.persistence.EmbeddedId
import jakarta.persistence.Entity
import jakarta.persistence.Table

@Entity
@Table(name = "orders")
class Order {

    @EmbeddedId
    OrderId id

    String customer
}

If the application works on the JVM but fails only once Hibernate starts inspecting metadata or creating proxies in the native executable, check these three areas first:

  • Entity scanning is not using classpath scanning

  • Lazy association targets are annotated with @GenerateProxy

  • Embeddable identifier types and other reflectively accessed model types are annotated with @ReflectiveAccess

5.6 Configuring Hibernate Reactive

It’s possible to use Hibernate Reactive by adding following dependency:

implementation("io.micronaut.sql:micronaut-hibernate-reactive")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-hibernate-reactive</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-hibernate-reactive",
]

and the Micronaut Data Transaction Hibernate dependency:

implementation("io.micronaut.data:micronaut-data-tx-hibernate")
<dependency>
    <groupId>io.micronaut.data</groupId>
    <artifactId>micronaut-data-tx-hibernate</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.data:micronaut-data-tx-hibernate",
]

Hibernate Reactive requires Java 11

To enable reactive session factory JPA configuration needs to have reactive property set to true. The reactive implementation doesn’t use traditional JDBC drivers, but instead it uses Vertx Drivers.

You can add one of:

implementation("io.vertx:vertx-mysql-client")
<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-mysql-client</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.vertx:vertx-mysql-client",
]

implementation("io.vertx:vertx-pg-client")
<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-pg-client</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.vertx:vertx-pg-client",
]

implementation("io.vertx:vertx-mssql-client")
<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-mssql-client</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.vertx:vertx-mssql-client",
]

implementation("io.vertx:vertx-oracle-client")
<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-oracle-client</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.vertx:vertx-oracle-client",
]

And configure it using properties:

jpa.default.reactive=true
jpa.default.properties.hibernate.connection.url=jdbc:postgresql:database
jpa.default.properties.hibernate.connection.username=myUsername
jpa.default.properties.hibernate.connection.password=myPassword
jpa:
  default:
    reactive: true
    properties:
      hibernate:
        connection:
          url: jdbc:postgresql:database # Use JDBC style url
          username: myUsername
          password: myPassword
[jpa.default]
reactive = true

[jpa.default.properties.hibernate.connection]
url = "jdbc:postgresql:database"
username = "myUsername"
password = "myPassword"
jpa {
  'default' {
    reactive = true
    properties {
      hibernate {
        connection {
          url = "jdbc:postgresql:database"
          username = "myUsername"
          password = "myPassword"
        }
      }
    }
  }
}
{
  jpa {
    default {
      reactive = true
      properties {
        hibernate {
          connection {
            url = "jdbc:postgresql:database"
            username = "myUsername"
            password = "myPassword"
          }
        }
      }
    }
  }
}
{
  "jpa": {
    "default": {
      "reactive": true,
      "properties": {
        "hibernate": {
          "connection": {
            "url": "jdbc:postgresql:database",
            "username": "myUsername",
            "password": "myPassword"
          }
        }
      }
    }
  }
}

The other option is to include one of the existing Micronaut SQL support libraries:

implementation("io.micronaut.sql:micronaut-vertx-mysql-client")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-vertx-mysql-client</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-vertx-mysql-client",
]

implementation("io.micronaut.sql:micronaut-vertx-pg-client")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-vertx-pg-client</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-vertx-pg-client",
]

And configure the client:

vertx.pg.client.port=5432
vertx.pg.client.host=the-host
vertx.pg.client.database=the-db
vertx.pg.client.user=user
vertx.pg.client.password=secret
vertx.pg.client.max-size=10
vertx:
  pg:
    client:
      port: 5432
      host: 'the-host'
      database: 'the-db'
      user: 'user'
      password: 'secret'
      max-size:  10
[vertx.pg.client]
port = 5432
host = "the-host"
database = "the-db"
user = "user"
password = "secret"
max-size = 10
vertx {
  pg {
    client {
      port = 5432
      host = "the-host"
      database = "the-db"
      user = "user"
      password = "secret"
      maxSize = 10
    }
  }
}
{
  vertx {
    pg {
      client {
        port = 5432
        host = "the-host"
        database = "the-db"
        user = "user"
        password = "secret"
        max-size = 10
      }
    }
  }
}
{
  "vertx": {
    "pg": {
      "client": {
        "port": 5432,
        "host": "the-host",
        "database": "the-db",
        "user": "user",
        "password": "secret",
        "max-size": 10
      }
    }
  }
}

The integration will automatically integrate Vertx driver instance of io.vertx.sqlclient.Pool found in the bean context.

5.7 Using compile-time Hibernate proxies

Hibernate uses a proxy object to implement lazy loading with a default implementation generating a proxy during the runtime.

This has a few disadvantages:

  • Runtime class generation can affect startup and runtime performance

  • Environments like GraalVM don’t support it

If you wish to use lazy entity associations and avoid runtime proxies you can enable compile-time proxies:

jpa.default.compile-time-hibernate-proxies=true
jpa:
  default:
    compile-time-hibernate-proxies: true
[jpa.default]
compile-time-hibernate-proxies = true
jpa {
  'default' {
    compileTimeHibernateProxies = true
  }
}
{
  jpa {
    default {
      compile-time-hibernate-proxies = true
    }
  }
}
{
  "jpa": {
    "default": {
      "compile-time-hibernate-proxies": true
    }
  }
}

Compile-time proxies require every entity type that Hibernate may proxy to be annotated with @GenerateProxy:

For example:

import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;

@Entity
public class Pet {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    private Owner owner;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Owner getOwner() {
        return owner;
    }

    public void setOwner(Owner owner) {
        this.owner = owner;
    }
}
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id
import jakarta.persistence.ManyToOne

@Entity
open class Pet {

    @Id
    @GeneratedValue
    open var id: Long? = null

    open var name: String? = null

    @ManyToOne(fetch = FetchType.LAZY)
    open var owner: Owner? = null
}
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id
import jakarta.persistence.ManyToOne

@Entity
class Pet {

    @Id
    @GeneratedValue
    Long id

    String name

    @ManyToOne(fetch = FetchType.LAZY)
    Owner owner
}

The entity Owner needs to be annotated with @GenerateProxy to have a proxy generated at compile time.

import io.micronaut.configuration.hibernate.jpa.proxy.GenerateProxy;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
@GenerateProxy
public class Owner {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
import io.micronaut.configuration.hibernate.jpa.proxy.GenerateProxy
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id

@Entity
@GenerateProxy
open class Owner {

    @Id
    @GeneratedValue
    open var id: Long? = null

    open var name: String? = null
}
import io.micronaut.configuration.hibernate.jpa.proxy.GenerateProxy
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id

@Entity
@GenerateProxy
class Owner {

    @Id
    @GeneratedValue
    Long id

    String name
}
For GraalVM native image, enable compile-time proxies with the configuration shown above.

For the full set of Hibernate native image requirements, including entity scanning and reflective access for embeddable identifier types, see the GraalVM native image section in this chapter.

5.8 Understanding LazyInitializationException

Micronaut is built on Netty which is based on a non-blocking, event loop model. JDBC and Hibernate are blocking APIs and hence when they are used in a Micronaut application the work is shifted to a blocking I/O thread pool.

When using jakarta.transaction.Transactional (or io.micronaut.transaction.annotation.Transactional) the Hibernate Session will only be open for the duration of this method execution and then will automatically be closed. This ensures that the blocking operation is kept as short as possible.

There is no notion of OpenSessionInView (OSIV) in Micronaut and never will be, since it is sub-optimal and not recommended. You should optimize the queries that you write to return all the necessary data Micronaut will need to encode your objects into JSON either by using the appropriate join queries or using a data transfer object (DTO).

If you encounter a LazyInitializationException when returning a Hibernate entity from a method it is an indication that your query is suboptimal and you should perform a join.

5.9 Vector types

Hibernate maps vector columns, such as the Oracle Database VECTOR type and PostgreSQL pgvector, with the hibernate-vector module. Micronaut SQL manages its version along with Hibernate:

implementation("org.hibernate.orm:hibernate-vector")
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-vector</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "org.hibernate.orm:hibernate-vector",
]

With the module on the classpath, a vector attribute is mapped with @JdbcTypeCode(SqlTypes.VECTOR) and its dimensions with @Array(length = …​), and vector distance functions such as cosine_distance and euclidean_distance are available in HQL and criteria queries. See the Hibernate ORM documentation for the mapping, the supported databases and the functions.

6 Configuring JAsync SQL

Micronaut supports asynchronous access to PostgreSQL and MySQL using jasync-sql, allowing to handle many database connections with a single thread.

6.1 Configuring jasync-sql Client

Using the CLI

If you are creating your project using the Micronaut CLI, supply the jasync-sql feature to configure the Jasync PostgreSQL and MySQL client in your project:

$ mn create-app my-app --features jasync-sql

To configure the Jasync client you should first add jasync-sql module to your classpath:

implementation("io.micronaut.sql:micronaut-jasync-sql")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jasync-sql</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-jasync-sql",
]

You should then configure the PoolOptions of the database server you wish to communicate with:

jasync.client.port=5432
jasync.client.host=the-host
jasync.client.database=the-db
jasync.client.username=test
jasync.client.password=test
jasync.client.maxActiveConnections=5
jasync:
    client:
        port: 5432
        host: the-host
        database: the-db
        username: test
        password: test
        maxActiveConnections: 5
[jasync.client]
port = 5432
host = "the-host"
database = "the-db"
username = "test"
password = "test"
maxActiveConnections = 5
jasync {
  client {
    port = 5432
    host = "the-host"
    database = "the-db"
    username = "test"
    password = "test"
    maxActiveConnections = 5
  }
}
{
  jasync {
    client {
      port = 5432
      host = "the-host"
      database = "the-db"
      username = "test"
      password = "test"
      maxActiveConnections = 5
    }
  }
}
{
  "jasync": {
    "client": {
      "port": 5432,
      "host": "the-host",
      "database": "the-db",
      "username": "test",
      "password": "test",
      "maxActiveConnections": 5
    }
  }
}

Once you have the above configuration in place then you can inject the com.github.jasync.sql.db.Connection bean. The following is the simplest way to connect:

result = client.sendQuery('SELECT * FROM pg_stat_database').thenApply({ QueryResult resultSet -> (1)
    return "Size: ${resultSet.rows.size()}"
}).get()
1 client is an instance of the com.github.jasync.sql.db.Connection bean.

For more information on running queries on using the client please read the "Running queries" section in the documentation of jasync-sql.

To use Jasync query interceptors register beans of type com.github.jasync.sql.db.interceptor.QueryInterceptor.

6.2 Database Health Checks

When the jasync-sql module is activated a JasyncHealthIndicator is activated resulting in the /health endpoint and CurrentHealthStatus interface resolving the health of the connection.

The only configuration option supported is to enable or disable the indicator by the endpoints.health.jasync.enabled key.

See the section on the Health Endpoint for more information.

7 Configuring jOOQ

Micronaut supports automatically configuring jOOQ library for fluent, typesafe SQL query construction.

To configure jOOQ library you should first add jooq module to your classpath:

implementation("io.micronaut.sql:micronaut-jooq")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jooq</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-jooq",
]

You should then either configure one or many DataSources or configure one or many R2DBC ConnectionFactory instances. For each registered DataSource or ConnectionFactory, Micronaut will configure the following jOOQ beans using JooqConfigurationFactory or R2dbcJooqConfigurationFactory:

For JDBC-backed jOOQ applications that use Micronaut transaction management, configure Transaction Management.

If Spring transaction management is in use, it will additionally create the following beans :

7.1 Configuring R2DBC connection factories

Micronaut also configures jOOQ for each named ConnectionFactory when R2DBC is on the classpath. Use the same logical data source name for both r2dbc.datasources. and jooq.r2dbc-datasources..

r2dbc.datasources.default.db-type=postgres
r2dbc.datasources.default.options.driver=pool
r2dbc.datasources.default.options.protocol=postgresql
jooq.r2dbc-datasources.default.sql-dialect=postgres
r2dbc:
  datasources:
    default:
      db-type: postgres
      options:
        driver: pool
        protocol: postgresql
jooq:
  r2dbc-datasources:
    default:
      sql-dialect: postgres
[r2dbc.datasources.default]
db-type = "postgres"

[r2dbc.datasources.default.options]
driver = "pool"
protocol = "postgresql"

[jooq.r2dbc-datasources.default]
sql-dialect = "postgres"
r2dbc {
  datasources {
    'default' {
      dbType = "postgres"
      options {
        driver = "pool"
        protocol = "postgresql"
      }
    }
  }
}
jooq {
  r2dbcDatasources {
    'default' {
      sqlDialect = "postgres"
    }
  }
}
{
  r2dbc {
    datasources {
      default {
        db-type = "postgres"
        options {
          driver = "pool"
          protocol = "postgresql"
        }
      }
    }
  }
  jooq {
    r2dbc-datasources {
      default {
        sql-dialect = "postgres"
      }
    }
  }
}
{
  "r2dbc": {
    "datasources": {
      "default": {
        "db-type": "postgres",
        "options": {
          "driver": "pool",
          "protocol": "postgresql"
        }
      }
    }
  },
  "jooq": {
    "r2dbc-datasources": {
      "default": {
        "sql-dialect": "postgres"
      }
    }
  }
}

The sql-dialect setting is optional. When it is not configured, jOOQ falls back to DEFAULT.

🔗
Table 1. Configuration Properties for R2dbcJooqConfigurationProperties
Property Type Description Default value

jooq.r2dbc-datasources.*.sql-dialect

org.jooq.SQLDialect

SQL dialect to use. Will be detected automatically by default.

jooq.r2dbc-datasources.*.json-converter-enabled

boolean

Set if enable {@link JsonConverterProvider} bean to use Jackson for JSON and JSONB types.

7.2 Configuring SQL dialect

Micronaut will attempt to detect database SQLDialect automatically.

If this does not work as desired, SQL dialect can be provided manually via configuration properties. The following example configures dialect for default datasource:

Configuring SQL dialect
jooq.datasources.default.sql-dialect=POSTGRES
jooq:
    datasources:
        default:
            sql-dialect: 'POSTGRES'
[jooq.datasources.default]
sql-dialect = "POSTGRES"
jooq {
  datasources {
    'default' {
      sqlDialect = "POSTGRES"
    }
  }
}
{
  jooq {
    datasources {
      default {
        sql-dialect = "POSTGRES"
      }
    }
  }
}
{
  "jooq": {
    "datasources": {
      "default": {
        "sql-dialect": "POSTGRES"
      }
    }
  }
}

7.3 Configuring additional provider beans

You can define additional beans which will be used when jOOQ Configuration is created. Only beans with the same name qualifier as the data source name will be used.

Micronaut will look for the following bean types:

7.4 Using JsonMapper to convert JSON(B) types

If you don’t register bean of type ConverterProvider and provide following configuration, JsonConverterProvider will be used, which uses Micronaut configured JsonMapper for converting from and to types JSON and JSONB.

Configuring Micronaut Json converter
jooq.datasources.default.json-converter-enabled=true
jooq:
    datasources:
        default:
            json-converter-enabled: true
[jooq.datasources.default]
json-converter-enabled = true
jooq {
  datasources {
    'default' {
      jsonConverterEnabled = true
    }
  }
}
{
  jooq {
    datasources {
      default {
        json-converter-enabled = true
      }
    }
  }
}
{
  "jooq": {
    "datasources": {
      "default": {
        "json-converter-enabled": true
      }
    }
  }
}

7.5 GraalVM native image

To use JOOQ in a native image it is necessary to declare the Record classes for reflection. The easiest way to do it is configure jOOQ to annotate the generated classes with the JPA annotations enabling the option jpaAnnotations. This way Micronaut will be able to detect them and automatically generate the reflection configuration that GraalVM needs.

For example, if you are using this gradle plugin you can add the following:

jooq {
    devDb(sourceSets.main) {
        ...
        generator {
            ...
            generate {
                jpaAnnotations = true (1)
            }
        }
    }
}
1 Configure jOOQ to generate the JPA annotations.

There is also built-in support for using SimpleFlatMapper with jOOQ in a native-image. No additional configuration is needed, just adding the SimpleFlatMapper dependency:

implementation("org.simpleflatmapper:sfm-jdbc:8.2.3")
<dependency>
    <groupId>org.simpleflatmapper</groupId>
    <artifactId>sfm-jdbc</artifactId>
    <version>8.2.3</version>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "org.simpleflatmapper:sfm-jdbc:8.2.3",
]

Find more information in the jOOQ documentation.

8 Configuring Jdbi

Micronaut supports automatically configuring Jdbi library for convenient, idiomatic access to relational data.

To configure the Jdbi library you should first add the jdbi module to your classpath:

implementation("io.micronaut.sql:micronaut-jdbi")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jdbi</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-jdbi",
]

If you need additional Jdbi modules such as jdbi3-sqlobject, Gradle consumers can declare them without an explicit version because micronaut-jdbi publishes the Jdbi BOM through Gradle module metadata:

implementation("org.jdbi:jdbi3-sqlobject")
<dependency>
    <groupId>org.jdbi</groupId>
    <artifactId>jdbi3-sqlobject</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "org.jdbi:jdbi3-sqlobject",
]

For Maven, import micronaut-sql-bom in dependencyManagement before declaring extra Jdbi modules, because Maven does not consume transitive BOM imports from regular dependencies:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.micronaut.sql</groupId>
      <artifactId>micronaut-sql-bom</artifactId>
      <version>${micronaut.sql.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jdbi</artifactId>
  </dependency>
  <dependency>
    <groupId>org.jdbi</groupId>
    <artifactId>jdbi3-sqlobject</artifactId>
  </dependency>
</dependencies>

You should then configure one or many DataSources. For each registered DataSource, Micronaut will configure the following Jdbi beans using JdbiFactory:

  • Jdbi - the Jdbi instance

For Jdbi applications that use Micronaut transaction management, configure Transaction Management.

If Spring transaction management is in use, it will additionally create the following beans :

8.1 Configuring additional provider beans

You can define additional beans which will be used when the Jdbi object is created. Only beans with a Named qualifier name with the same name as the data source name will be used.

Micronaut will look for the following bean types:

9 Configuring MyBatis

Micronaut supports automatically configuring MyBatis for SQL mappers built on top of JDBC.

To configure MyBatis you should first add the mybatis module to your classpath:

implementation("io.micronaut.sql:micronaut-mybatis")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-mybatis</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-mybatis",
]

MyBatis support builds on Micronaut JDBC. In addition to micronaut-mybatis, add one of the JDBC runtime modules and a JDBC driver dependency.

Once those dependencies are present, configure one or many DataSources. For example:

datasources.default.url=jdbc:h2:mem:devDb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
datasources.default.driverClassName=org.h2.Driver
datasources.default.username=sa
datasources.default.password=
datasources:
  default:
    url: jdbc:h2:mem:devDb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    driverClassName: org.h2.Driver
    username: sa
    password: ''
[datasources.default]
url = "jdbc:h2:mem:devDb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"
driverClassName = "org.h2.Driver"
username = "sa"
password = ""
datasources {
  'default' {
    url = "jdbc:h2:mem:devDb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"
    driverClassName = "org.h2.Driver"
    username = "sa"
    password = ""
  }
}
{
  datasources {
    default {
      url = "jdbc:h2:mem:devDb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"
      driverClassName = "org.h2.Driver"
      username = "sa"
      password = ""
    }
  }
}
{
  "datasources": {
    "default": {
      "url": "jdbc:h2:mem:devDb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
      "driverClassName": "org.h2.Driver",
      "username": "sa",
      "password": ""
    }
  }
}

For each registered DataSource, Micronaut will configure the following MyBatis beans using MyBatisFactory:

If no MyBatis transaction factory is provided for a DataSource, Micronaut uses JdbcTransactionFactory.

When multiple datasources are configured, Micronaut qualifies each MyBatis bean with the datasource name. You can then inject the Configuration, SqlSessionFactory, or SqlSessionManager for a specific datasource with @Named.

Mapper interfaces are not registered automatically. You can register mappers and apply additional configuration through customizer beans, then obtain them from the corresponding SqlSessionManager.

For a complete application example, see the Micronaut Data access MyBatis guide.

9.1 Injecting MyBatis runtime beans

Once a datasource is configured, you can inject the MyBatis runtime beans that Micronaut creates for it.

For a single datasource application, injecting MyBatisFactory output beans requires no qualifier.

If you configure multiple datasources, inject the matching bean with @Named annotation.

The SqlSessionManager is the typical entry point for mapper usage. Retrieve mapper implementations with getMapper(..) after registering the mapper interface through a customizer.

9.2 Configuring MyBatis customizers

You can define additional beans which will be used when the MyBatis Configuration is created. Only beans of type MyBatisConfigurationCustomizer with an @Named qualifier matching the datasource name will be applied.

For example, you can register mapper interfaces and tune MyBatis settings for a specific datasource:

import io.micronaut.configuration.mybatis.MyBatisConfigurationCustomizer;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import org.apache.ibatis.session.Configuration;

@Named("default")
@Singleton
public class CustomConfigurationCustomizer implements MyBatisConfigurationCustomizer {
    @Override
    public void customize(Configuration configuration) {
        configuration.addMappers("example.micronaut.mappers");
        configuration.setMapUnderscoreToCamelCase(true);
    }
}
from jakarta.inject import Named, Singleton
from micronaut.configuration.mybatis import MyBatisConfigurationCustomizer
from org.apache.ibatis.session import Configuration

@Named("default")
@Singleton
class CustomConfigurationCustomizer(MyBatisConfigurationCustomizer):
    def customize(self, configuration: Configuration) -> None:
        configuration.addMappers("example.micronaut.mappers")
        configuration.setMapUnderscoreToCamelCase(True)
import io.micronaut.configuration.mybatis.MyBatisConfigurationCustomizer
import jakarta.inject.Named
import jakarta.inject.Singleton
import org.apache.ibatis.session.Configuration

@Named("default")
@Singleton
class CustomConfigurationCustomizer : MyBatisConfigurationCustomizer {
    override fun customize(configuration: Configuration) {
        configuration.addMappers("example.micronaut.mappers")
        configuration.isMapUnderscoreToCamelCase = true
    }
}
import io.micronaut.configuration.mybatis.MyBatisConfigurationCustomizer
import jakarta.inject.Named
import jakarta.inject.Singleton
import org.apache.ibatis.session.Configuration

@Named("default")
@Singleton
class CustomConfigurationCustomizer implements MyBatisConfigurationCustomizer {
    @Override
    void customize(Configuration configuration) {
        configuration.addMappers("example.micronaut.mappers")
        configuration.mapUnderscoreToCamelCase = true
    }
}

This hook is also the right place to configure other MyBatis features that belong on the Configuration, such as type aliases, type handlers, interceptors, or additional mapper registrations.

TransactionFactory Customization

Micronaut also looks for a TransactionFactory bean qualified with the datasource name. If none is present, Micronaut falls back to JdbcTransactionFactory.

10 Configuring Reactive MySQL Client

Micronaut supports reactive and non-blocking client to connect to MySQL using vertx-mysql-client, allowing to handle many database connections with a single thread.

Using the CLI

If you are creating your project using the Micronaut CLI, supply the vertx-mysql-client feature to configure the MySQL Vertx client in your project:

$ mn create-app my-app --features vertx-mysql-client

To configure the MySQL Vertx client you should first add vertx-mysql-client module to your classpath:

implementation("io.micronaut.sql:micronaut-vertx-mysql-client")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-vertx-mysql-client</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-vertx-mysql-client",
]

You should then configure the URI or MySQLConnectOptions,PoolOptions of the MySQL server you wish to communicate with in application.yml:

vertx.mysql.client.port=3306
vertx.mysql.client.host=the-host
vertx.mysql.client.database=the-db
vertx.mysql.client.user=test
vertx.mysql.client.password=test
vertx.mysql.client.maxSize=5
vertx:
  mysql:
    client:
      port: 3306
      host: the-host
      database: the-db
      user: test
      password: test
      maxSize: 5
[vertx.mysql.client]
port = 3306
host = "the-host"
database = "the-db"
user = "test"
password = "test"
maxSize = 5
vertx {
  mysql {
    client {
      port = 3306
      host = "the-host"
      database = "the-db"
      user = "test"
      password = "test"
      maxSize = 5
    }
  }
}
{
  vertx {
    mysql {
      client {
        port = 3306
        host = "the-host"
        database = "the-db"
        user = "test"
        password = "test"
        maxSize = 5
      }
    }
  }
}
{
  "vertx": {
    "mysql": {
      "client": {
        "port": 3306,
        "host": "the-host",
        "database": "the-db",
        "user": "test",
        "password": "test",
        "maxSize": 5
      }
    }
  }
}
You can also connect to MySQL using uri instead of the other properties.

Once you have the above configuration in place then you can inject the io.vertx.reactivex.mysqlclient.MySQLPool bean. The following is the simplest way to connect:

RowSet<Row> rowSet = client.query('SELECT * FROM foo').execute().toCompletionStage().toCompletableFuture().get() (1)
RowIterator<Row> iterator = rowSet.iterator()
int id = iterator.next().getInteger("id")
result = "id: ${id}"
1 client is an instance of the io.vertx.reactivex.mysqlclient.MySQLPool bean.

For more information on running queries on MySQL using the reactive client please read the "Running queries" section in the documentation of vertx-mysql-client.

10.1 MySQL Health Checks

When the vertx-mysql-client module is activated a MySQLClientPoolHealthIndicator is activated resulting in the /health endpoint and CurrentHealthStatus interface resolving the health of the MySQL connection.

The only configuration option supported is to enable or disable the indicator by the endpoints.health.vertx.mysql.client.enabled key.

See the section on the <Health Endpoint for more information.

11 Configuring Reactive PostgreSQL Client

Micronaut supports reactive and non-blocking client to connect to PostgreSQL using vertx-pg-client, allowing to handle many database connections with a single thread.

Using the CLI

If you are creating your project using the Micronaut CLI, supply the vertx-pg-client feature to configure the PostgreSQL Vertx client in your project:

$ mn create-app my-app --features vertx-pg-client

To configure the PostgreSQL Vertx client you should first add vertx-pg-client module to your classpath:

implementation("io.micronaut.sql:micronaut-vertx-pg-client")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-vertx-pg-client</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-vertx-pg-client",
]

You should then configure the URI or PgConnectOptions,PoolOptions of the PostgreSQL server you wish to communicate with:

vertx.pg.client.port=3306
vertx.pg.client.host=the-host
vertx.pg.client.database=the-db
vertx.pg.client.user=test
vertx.pg.client.password=test
vertx.pg.client.maxSize=5
vertx:
  pg:
    client:
      port: 3306
      host: the-host
      database: the-db
      user: test
      password: test
      maxSize: 5
[vertx.pg.client]
port = 3306
host = "the-host"
database = "the-db"
user = "test"
password = "test"
maxSize = 5
vertx {
  pg {
    client {
      port = 3306
      host = "the-host"
      database = "the-db"
      user = "test"
      password = "test"
      maxSize = 5
    }
  }
}
{
  vertx {
    pg {
      client {
        port = 3306
        host = "the-host"
        database = "the-db"
        user = "test"
        password = "test"
        maxSize = 5
      }
    }
  }
}
{
  "vertx": {
    "pg": {
      "client": {
        "port": 3306,
        "host": "the-host",
        "database": "the-db",
        "user": "test",
        "password": "test",
        "maxSize": 5
      }
    }
  }
}
You can also connect to PostgreSQL using uri instead of the other properties.

When you need to trust a PostgreSQL or CockroachDB CA certificate for SSL connections, configure PEM trust certificates with pem-trust-options:

vertx.pg.client.ssl=true
vertx.pg.client.ssl-mode=VERIFY_CA
vertx.pg.client.pem-trust-options.cert-paths[0]=certs/ca.crt
vertx:
  pg:
    client:
      ssl: true
      ssl-mode: VERIFY_CA
      pem-trust-options:
        cert-paths:
          - certs/ca.crt
[vertx.pg.client]
ssl = true
ssl-mode = "VERIFY_CA"

[vertx.pg.client.pem-trust-options]
cert-paths = ["certs/ca.crt"]
vertx {
  pg {
    client {
      ssl = true
      sslMode = "VERIFY_CA"
      pemTrustOptions {
        certPaths = ["certs/ca.crt"]
      }
    }
  }
}
{
  vertx {
    pg {
      client {
        ssl = true
        ssl-mode = "VERIFY_CA"
        pem-trust-options {
          cert-paths = ["certs/ca.crt"]
        }
      }
    }
  }
}
{
  "vertx": {
    "pg": {
      "client": {
        "ssl": true,
        "ssl-mode": "VERIFY_CA",
        "pem-trust-options": {
          "cert-paths": ["certs/ca.crt"]
        }
      }
    }
  }
}

Once you have the above configuration in place then you can inject the core io.vertx.sqlclient.Pool bean. If io.vertx:vertx-rx-java3 is on the classpath, Micronaut also exposes the RxJava3 io.vertx.rxjava3.sqlclient.Pool wrapper bean. The following is the simplest way to connect:

RowSet<Row> rowSet = client.query('SELECT * FROM pg_stat_database').execute().toCompletionStage().toCompletableFuture().get() (1)
int size = 0
RowIterator<Row> iterator = rowSet.iterator()
while (iterator.hasNext()) {
    iterator.next()
    size++
}
result = "Size: ${size}"
1 client is an instance of the io.vertx.sqlclient.Pool bean.

For more information on running queries on Postgres using the reactive client please read the "Running queries" section in the documentation of vertx-pg-client.

11.1 PostgreSQL Health Checks

When the vertx-pg-client module is activated a PgClientPoolHealthIndicator is activated resulting in the /health endpoint and CurrentHealthStatus interface resolving the health of the Postgres connection.

The only configuration option supported is to enable or disable the indicator by the endpoints.health.vertx.pg.client.enabled key.

See the section on the <Health Endpoint for more information.

12 Configuring Reactive Oracle Client

Micronaut supports the reactive and non-blocking Vert.x Oracle client, which runs on the asynchronous API of the Oracle JDBC driver.

To use it, add the vertx-oracle-client module to your classpath:

implementation("io.micronaut.sql:micronaut-vertx-oracle-client")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-vertx-oracle-client</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.sql:micronaut-vertx-oracle-client",
]

The module depends on the same Oracle JDBC driver (ojdbc17) as the other Micronaut SQL modules, instead of the older ojdbc11 release the Vert.x Oracle client declares.

Then configure the OracleConnectOptions and PoolOptions of the database:

vertx.oracle.client.host=the-host
vertx.oracle.client.port=1521
vertx.oracle.client.service-name=FREEPDB1
vertx.oracle.client.user=test
vertx.oracle.client.password=test
vertx.oracle.client.max-size=5
vertx:
  oracle:
    client:
      host: the-host
      port: 1521
      service-name: FREEPDB1
      user: test
      password: test
      max-size: 5
[vertx.oracle.client]
host = "the-host"
port = 1521
service-name = "FREEPDB1"
user = "test"
password = "test"
max-size = 5
vertx {
  oracle {
    client {
      host = "the-host"
      port = 1521
      serviceName = "FREEPDB1"
      user = "test"
      password = "test"
      maxSize = 5
    }
  }
}
{
  vertx {
    oracle {
      client {
        host = "the-host"
        port = 1521
        service-name = "FREEPDB1"
        user = "test"
        password = "test"
        max-size = 5
      }
    }
  }
}
{
  "vertx": {
    "oracle": {
      "client": {
        "host": "the-host",
        "port": 1521,
        "service-name": "FREEPDB1",
        "user": "test",
        "password": "test",
        "max-size": 5
      }
    }
  }
}

You can also set uri instead of the host, port and service name, for example oracle:thin:@the-host:1521/FREEPDB1, a JDBC URL such as jdbc:oracle:thin:@the-host:1521/FREEPDB1, or a TNS alias such as oracle:thin:@mydb_high. The URI replaces the configured host, port, service name, SID and TNS alias, and the other configured options, such as user, password and tns-admin, still apply unless the URI sets them. Connection properties in the URI are added to the configured properties.

The Oracle client connects through the Oracle JDBC driver, so Vert.x network options such as connect timeouts and TLS stores don’t apply: configure those through the connection properties or the Oracle Net configuration instead. When there is no io.vertx.core.Vertx bean, the module creates a Vert.x instance for the pool and closes it with the application context.

The module then provides an io.vertx.sqlclient.Pool bean, which you can inject to run queries:

import io.vertx.sqlclient.Pool;
import io.vertx.sqlclient.Tuple;
import jakarta.inject.Singleton;

import java.util.concurrent.CompletionStage;

@Singleton
public class BookCatalog {

    private final Pool pool; // (1)

    public BookCatalog(Pool pool) {
        this.pool = pool;
    }

    public CompletionStage<String> findTitle(long id) {
        return pool.preparedQuery("SELECT title FROM book WHERE id = ?") // (2)
            .execute(Tuple.of(id))
            .map(rows -> rows.iterator().next().getString("TITLE"))
            .toCompletionStage();
    }
}
from typing import Annotated

from io.vertx.sqlclient import Pool, Tuple
from jakarta.inject import Inject, Singleton
from java.util.concurrent import CompletionStage

@Singleton
class BookCatalog:
    pool: Annotated[Pool, Inject]  # (1)

    def find_title(self, id: int) -> CompletionStage:
        return (self.pool.preparedQuery("SELECT title FROM book WHERE id = ?")  # (2)
                .execute(Tuple.of(id))
                .map(lambda rows: rows.iterator().next().getString("TITLE"))
                .toCompletionStage())
import io.vertx.sqlclient.Pool
import io.vertx.sqlclient.Tuple
import jakarta.inject.Singleton
import java.util.concurrent.CompletionStage

@Singleton
class BookCatalog(private val pool: Pool) { // (1)

    fun findTitle(id: Long): CompletionStage<String> =
        pool.preparedQuery("SELECT title FROM book WHERE id = ?") // (2)
            .execute(Tuple.of(id))
            .map { rows -> rows.iterator().next().getString("TITLE") }
            .toCompletionStage()
}
import io.vertx.sqlclient.Pool
import io.vertx.sqlclient.Tuple
import jakarta.inject.Singleton

import java.util.concurrent.CompletionStage

@Singleton
class BookCatalog {

    private final Pool pool // (1)

    BookCatalog(Pool pool) {
        this.pool = pool
    }

    CompletionStage<String> findTitle(long id) {
        pool.preparedQuery("SELECT title FROM book WHERE id = ?") // (2)
            .execute(Tuple.of(id))
            .map { rows -> rows.iterator().next().getString("TITLE") }
            .toCompletionStage()
    }
}
1 The Pool bean configured by the vertx.oracle.client properties
2 A prepared query with a bind parameter, executed without blocking

For more information on running queries with the reactive client, read the vertx-oracle-client documentation.

12.1 Oracle Health Checks

When the vertx-oracle-client module is configured, a OracleHealthIndicator is activated, and the /health endpoint and the CurrentHealthStatus interface report the health of the Oracle connection, with the database version as a detail.

The only configuration option is to enable or disable the indicator with the endpoints.health.vertx.oracle.client.enabled key.

See the section on the Health Endpoint for more information.

13 Repository

You can find the source code of this project in this repository: