Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Micronaut Data JDBC and H2

Articles kept in memory vanish on every restart. Time to store them in a database. Micronaut Data is the data access module: you describe a repository interface, and the annotation processor generates the implementation with ready SQL queries at compile time. This course uses Micronaut Data JDBC, which works on top of plain JDBC, without Hibernate. The database is H2: it lives in memory and needs no installation.

Dependencies

mn feature-diff --features=data-jdbc,h2 shows the same changes. Add to <dependencies>:

<!-- pom.xml -->
<dependency>
  <groupId>io.micronaut.data</groupId>
  <artifactId>micronaut-data-jdbc</artifactId>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>io.micronaut.sql</groupId>
  <artifactId>micronaut-jdbc-hikari</artifactId>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>runtime</scope>
</dependency>

In <annotationProcessorPaths> add a <path> with io.micronaut.data:micronaut-data-processor and the version ${micronaut.data.version}, exactly like the validation processor in the previous chapter. micronaut-jdbc-hikari provides the HikariCP connection pool.

Data source settings

# src/main/resources/application.properties
datasources.default.url=jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
datasources.default.driver-class-name=org.h2.Driver
datasources.default.username=sa
datasources.default.password=
datasources.default.dialect=H2
datasources.default.schema-generate=CREATE_DROP

schema-generate=CREATE_DROP creates tables from the entities at startup and drops them at shutdown, which is handy for learning. A real project manages the schema with migrations (the flyway or liquibase features).

The entity

The Article record becomes a row of the article table:

// src/main/java/com/example/Article.java
package com.example;
 
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.serde.annotation.Serdeable;
 
@Serdeable
@MappedEntity
public record Article(
        @Id @GeneratedValue @Nullable Long id,
        String title,
        String body) {
}

@MappedEntity links the record to a table, @Id marks the primary key, and @GeneratedValue says the database assigns it. @Nullable is needed because a new article has no id yet.

The repository

// src/main/java/com/example/ArticleRepository.java
package com.example;
 
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;
 
@JdbcRepository(dialect = Dialect.H2)
public interface ArticleRepository extends CrudRepository<Article, Long> {
}
CrudRepository methodWhat it does
save(entity)INSERT, returns the record with its new id
update(entity)UPDATE by id
findById(id)An Optional with the record
findAll()A list of all records
existsById(id), count()Check and count
deleteById(id)DELETE

A service on top of the repository

The service no longer keeps a Map; it receives the repository through its constructor:

// src/main/java/com/example/ArticleService.java
@Singleton
public class ArticleService {
 
    private final ArticleRepository articleRepository;
 
    public ArticleService(ArticleRepository articleRepository) {
        this.articleRepository = articleRepository;
    }
 
    public List<Article> findAll() {
        return articleRepository.findAll();
    }
 
    public Optional<Article> findById(Long id) {
        return articleRepository.findById(id);
    }
 
    public Article create(ArticleForm form) {
        return articleRepository.save(new Article(null, form.title(), form.body()));
    }
}

The update and delete methods work the same way: they call articleRepository.update(...) and articleRepository.deleteById(id), and the controller gets @Put("/{id}") and @Delete("/{id}") routes.

Database calls block the thread, and by default Micronaut handles requests on Netty event loop threads. So add @ExecuteOn(TaskExecutors.BLOCKING) (package io.micronaut.scheduling) above ArticleController: the controller methods will then run in a separate pool, which on Java 25 means virtual threads.

Trying it out

To see the SQL, add the line <logger name="io.micronaut.data.query" level="debug"/> to src/main/resources/logback.xml. At startup the log shows:

DEBUG io.micronaut.data.query - Executing CREATE statement:
CREATE TABLE `article` (`id` BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,`title` VARCHAR(255) NOT NULL,`body` VARCHAR(255) NOT NULL);

POST /articles from the earlier lessons now runs INSERT INTO article, and the response {"id":1,"title":"Hello, Micronaut","body":"First article"} carries the number assigned by the database.

Common mistake

If the micronaut-data-processor is not registered, the application starts, but the first request fails:

ERROR i.m.http.server.RouteExecutor - Unexpected error occurred: Micronaut Data method is missing compilation time query information.
Ensure that the Micronaut Data annotation processors are declared in your build and try again with a clean re-build.

Add the processor and run ./mvnw clean package, just as the message suggests.

Thanks for reading!

Donate to the site — we work hard for you.