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_DROPschema-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 method | What 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.