Micronaut SQL Libraries

Projects to support SQL Database access in Micronaut

Version: 7.2.1-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 = {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
    }
  }
}
{
  "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.

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"}, 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
        }
      }
    }
  }
}
{
  "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", 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, it checks if either password or username change have been sent in refresh event and builds a change set per datasource

  • Implementations for the supported pools set the new username and/or password on the pool and trigger a soft eviction of connections so subsequent connections use the updated credentials.

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
            applicationContext.publishEvent(new RefreshEvent(Map.of("datasources.default.password", password)));
            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
            self.application_context.publishEvent(RefreshEvent({"datasources.default.password": password}))
            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
            applicationContext.publishEvent(RefreshEvent(mapOf<String, Any>("datasources.default.password" to password)))
            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
            applicationContext.publishEvent(new RefreshEvent(["datasources.default.password": password]))
            this.currentPassword = password
        }
    }

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

Alternative way to implement password rotation:

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.Map;

@Singleton
final class DbPasswordRefresher {

    private final ApplicationContext applicationContext;

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

    @Scheduled(cron = "0 * * * * *")
    void refresh() {
        // refreshAndDiff() will register if there were changes in config values (Vault, Secret, etc.),
        // update application configuration and populate the changes map.
        Map<String, Object> changes = applicationContext.getEnvironment().refreshAndDiff();
        // ${DB_PASSWORD} placeholder in refreshAndDiff() call will be populated as `db-password` key in changes map
        if (changes.containsKey("db-password")) {
            String password = (String) changes.get("db-password");
            // The datasource event handler for this event will get actual password from the
            // application configuration that has been refreshed in refreshAndDiff() call above
            applicationContext.publishEvent(new RefreshEvent(Map.of("datasources.default.password", password)));
        }
    }
}
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() will register if there were changes in config values (Vault, Secret, etc.),
        # update application configuration and populate the changes map.
        changes = self.application_context.getEnvironment().refreshAndDiff()
        # ${DB_PASSWORD} placeholder in refreshAndDiff() call will be populated as `db-password` key in changes map
        if changes.containsKey("db-password"):
            password = changes.get("db-password")
            # The datasource event handler for this event will get actual password from the
            # application configuration that has been refreshed in refreshAndDiff() call above
            self.application_context.publishEvent(RefreshEvent({"datasources.default.password": password}))
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() will register if there were changes in config values (Vault, Secret, etc.),
        // update application configuration and populate the changes map.
        val changes = applicationContext.environment.refreshAndDiff()
        // ${DB_PASSWORD} placeholder in refreshAndDiff() call will be populated as `db-password` key in changes map
        if (changes.containsKey("db-password")) {
            val password = changes["db-password"] as String
            // The datasource event handler for this event will get actual password from the
            // application configuration that has been refreshed in refreshAndDiff() call above
            applicationContext.publishEvent(RefreshEvent(mapOf<String, Any>("datasources.default.password" to password)))
        }
    }
}
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() will register if there were changes in config values (Vault, Secret, etc.),
        // update application configuration and populate the changes map.
        Map<String, Object> changes = applicationContext.environment.refreshAndDiff()
        // ${DB_PASSWORD} placeholder in refreshAndDiff() call will be populated as `db-password` key in changes map
        if (changes.containsKey("db-password")) {
            String password = (String) changes.get("db-password")
            // The datasource event handler for this event will get actual password from the
            // application configuration that has been refreshed in refreshAndDiff() call above
            applicationContext.publishEvent(new RefreshEvent(["datasources.default.password": password]))
        }
    }
}

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.

  • Existing connections are softly evicted where supported by the pool; new connections will be established with the updated credentials.

  • Supported pools: HikariCP, Tomcat JDBC, Oracle UCP.

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 service 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

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.

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"]}, properties = {hibernate = {hbm2ddl = {auto = "update"}, show_sql = true}}}}
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, 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.

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", 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", 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 Repository

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