Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Transactions

A transaction is a group of database queries that either runs completely or not at all. An example from the blog: publishing an article together with its first comment. If the article is saved but the comment is not, half an operation stays in the database. In the JDBC lesson you handled this by hand with commit() and rollback(). In Micronaut, the @Transactional annotation on a bean method is enough.

A method in a transaction

Add the comment repository to ArticleService (as the second constructor parameter) and a new method:

// src/main/java/com/example/ArticleService.java
import jakarta.transaction.Transactional;
 
@Transactional
public Article publish(ArticleForm form, String firstComment) {
    Article article = articleRepository.save(new Article(null, form.title(), form.body()));
    if (firstComment.isBlank()) {
        throw new IllegalArgumentException("The first comment is empty");
    }
    commentRepository.save(new Comment(null, article.id(), "Anna", firstComment));
    return article;
}

Before the method runs, Micronaut opens a transaction. If the method finishes normally, it commits. If an unchecked exception (a subclass of RuntimeException) escapes, it rolls back: the article saved in the first line disappears. The jakarta.transaction.Transactional annotation is already available together with micronaut-data-jdbc.

A publish controller

// src/main/java/com/example/PublishController.java
package com.example;
 
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
import io.micronaut.serde.annotation.Serdeable;
 
@ExecuteOn(TaskExecutors.BLOCKING)
@Controller("/publish")
public class PublishController {
 
    @Serdeable
    public record PublishForm(String title, String body, String firstComment) {
    }
 
    private final ArticleService articleService;
 
    public PublishController(ArticleService articleService) {
        this.articleService = articleService;
    }
 
    @Post
    public Article publish(@Body PublishForm form) {
        return articleService.publish(new ArticleForm(form.title(), form.body()), form.firstComment());
    }
}

Trying it out

First a successful publication:

curl -X POST http://localhost:8080/publish -H "Content-Type: application/json" \
  -d '{"title":"Transactions","body":"Fourth article","firstComment":"Welcome"}'
{"id":4,"title":"Transactions","body":"Fourth article"}

Now with an empty comment:

curl -X POST http://localhost:8080/publish -H "Content-Type: application/json" \
  -d '{"title":"Lost article","body":"Should roll back","firstComment":" "}'

The response is 500, and the SQL log shows that the INSERT into the article table did run:

DEBUG io.micronaut.data.query - Executing SQL query: INSERT INTO `article` (`title`,`body`) VALUES (?,?)
ERROR i.m.http.server.RouteExecutor - Unexpected error occurred: The first comment is empty

Yet curl http://localhost:8080/articles does not show the "Lost article": the transaction was rolled back. In a real API you would write an ExceptionHandler for this error and return 400.

Useful variants

AnnotationMeaning
@TransactionalOpen a transaction or join one that is already open
@Transactional(rollbackOn = IOException.class)Also roll back on this checked exception
@Transactional(Transactional.TxType.REQUIRES_NEW)Always a new, separate transaction
@ReadOnlyRead only (package io.micronaut.transaction.annotation)

Each repository method already runs in a transaction on its own. @Transactional is needed when several calls must become one unit.

Common mistake

The annotation works through an interceptor that Micronaut generates at compile time in a subclass of the bean. A private method cannot be overridden, so @Transactional on a private method is a build error:

[ERROR] ArticleService.java:[50,18] Method annotated as executable but is declared private.
Change the method to be non-private in order for AOP advice to be applied.

For the same reason the interceptor does not fire when the method is called from inside the same class through this. Make transactional methods public and call them from another bean.

Thanks for reading!

Donate to the site — we work hard for you.