Groovy / Gradle

Access a database with MyBatis

Learn how to access a database with MyBatis using Micronaut Framework.

Iván López, Sergio del Amo
On this guide
In this section

Getting Started

Learn how to access a database with MyBatis using Micronaut Framework.

What you will need

To complete this guide, you will need the following:

Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

Writing the Application

Create an application using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app example.micronaut.micronautguide \
    --features=flyway,jdbc-hikari,mybatis,serialization-jackson,validation \
    --build=gradle \
    --lang=groovy \
    --test=spock
Note
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool.
If you don’t specify the --lang argument, Java is used as the language.
If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.

The previous command creates a Micronaut application with the default package example.micronaut in a directory named micronautguide.

If you use Micronaut Launch, select Micronaut Application as application type and add flyway, jdbc-hikari, mybatis, serialization-jackson, and validation features.

Note
If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features, and apply those changes to your application.

Configure Data Source and JPA

Add the following snippet to include the necessary dependencies:

build.gradle

Define the data source in src/main/resources/application.properties.

src/main/resources/application.properties
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.driver-class-name=org.h2.Driver

MyBatis Configuration

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.

Create a bean to register additional mappers:

groovy/src/main/groovy/example/micronaut/CustomConfigurationCustomizer.groovy

Domain

Create the domain entities:

groovy/src/main/groovy/example/micronaut/domain/Genre.groovy
package example.micronaut.domain

import com.fasterxml.jackson.annotation.JsonIgnore
import groovy.transform.CompileStatic
import io.micronaut.core.annotation.NonNull
import io.micronaut.core.annotation.Nullable
import io.micronaut.serde.annotation.Serdeable

import jakarta.validation.constraints.NotBlank

@CompileStatic
@Serdeable
class Genre {

    @Nullable
    Long id

    @NonNull
    @NotBlank
    String name

    @NonNull
    @JsonIgnore
    Set<Book> books = []

    Genre(@NonNull @NotBlank String name) {
        this.name = name
    }

    @Override
    String toString() {
        "Genre{id=$id, name='$name', books=$books}"
    }
}
groovy/src/main/groovy/example/micronaut/domain/Book.groovy
package example.micronaut.domain

import groovy.transform.CompileStatic
import io.micronaut.core.annotation.NonNull
import io.micronaut.core.annotation.Nullable
import io.micronaut.serde.annotation.Serdeable
import jakarta.validation.constraints.NotBlank

@CompileStatic
@Serdeable
class Book {

    @Nullable
    Long id

    @NonNull
    @NotBlank
    String name

    @NonNull
    @NotBlank
    String isbn

    Genre genre

    Book(@NonNull @NotBlank String isbn,
         @NonNull @NotBlank String name,
         Genre genre) {
        this.isbn = isbn
        this.name = name
        this.genre = genre
    }

    @Override
    String toString() {
        "Book{id=$id, name='$name', isbn='$isbn', genre=$genre}"
    }
}

Repository Access

Create an interface to define the operations to access the database and use MyBatis annotations to map the methods to SQL queries:

groovy/src/main/groovy/example/micronaut/genre/GenreMapper.groovy
package example.micronaut.genre

import example.micronaut.domain.Genre
import org.apache.ibatis.annotations.Delete
import org.apache.ibatis.annotations.Insert
import org.apache.ibatis.annotations.Options
import org.apache.ibatis.annotations.Param
import org.apache.ibatis.annotations.Select
import org.apache.ibatis.annotations.Update

import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.Positive
import jakarta.validation.constraints.PositiveOrZero

interface GenreMapper {

    @Select('select * from genre where id=#{id}')
    Genre findById(long id)

    @Insert('insert into genre(name) values(#{name})')
    @Options(useGeneratedKeys = true, keyProperty = 'id')
    void save(Genre genre)

    @Delete('delete from genre where id=#{id}')
    void deleteById(long id)

    @Update('update genre set name=#{name} where id=#{id}')
    void update(@Param('id') long id, @Param('name') String name)

    @Select('select * from genre')
    List<Genre> findAll()

    @Select('select * from genre order by ${sort} ${order}')
    List<Genre> findAllBySortAndOrder(@NotNull @Pattern(regexp = 'id|name') String sort,
                                      @NotNull @Pattern(regexp = 'asc|ASC|desc|DESC') String order)

    @Select('select * from genre order by ${sort} ${order} limit ${offset}, ${max}')
    List<Genre> findAllByOffsetAndMaxAndSortAndOrder(@PositiveOrZero int offset,
                                                     @Positive int max,
                                                     @NotNull @Pattern(regexp = 'id|name') String sort,
                                                     @NotNull @Pattern(regexp = 'asc|ASC|desc|DESC') String order)

    @Select('select * from genre limit ${offset}, ${max}')
    List<Genre> findAllByOffsetAndMax(@PositiveOrZero int offset, @Positive int max)
}

And the implementation:

groovy/src/main/groovy/example/micronaut/genre/GenreMapperImpl.groovy

Create an interface to define the high-level operations exposed to the application:

groovy/src/main/groovy/example/micronaut/genre/GenreRepository.groovy
package example.micronaut.genre

import example.micronaut.ListingArguments
import example.micronaut.domain.Genre
import io.micronaut.core.annotation.NonNull

import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull

interface GenreRepository {

    @NonNull
    Optional<Genre> findById(long id)

    @NonNull
    Genre save(@NonNull @NotBlank String name)

    void deleteById(long id)

    @NonNull
    List<Genre> findAll(@NonNull @NotNull ListingArguments args)

    int update(long id, @NonNull @NotBlank String name)
}

And the implementation using GenreMapper:

groovy/src/main/groovy/example/micronaut/genre/GenreRepositoryImpl.groovy
package example.micronaut.genre

import example.micronaut.ListingArguments
import example.micronaut.domain.Genre
import groovy.transform.CompileStatic
import io.micronaut.core.annotation.NonNull
import jakarta.inject.Singleton

import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull

@CompileStatic
@Singleton // 
class GenreRepositoryImpl implements GenreRepository {

    private final GenreMapper genreMapper

    GenreRepositoryImpl(GenreMapper genreMapper) {
        this.genreMapper = genreMapper
    }

    @Override
    @NonNull
    Optional<Genre> findById(long id) {
        Optional.ofNullable(genreMapper.findById(id))
    }

    @Override
    @NonNull
    Genre save(@NonNull @NotBlank String name) {
        Genre genre = new Genre(name)
        genreMapper.save(genre)
        genre
    }

    @Override
    void deleteById(long id) {
        findById(id).ifPresent(genre -> genreMapper.deleteById(id))
    }

    @NonNull
    List<Genre> findAll(@NonNull @NotNull ListingArguments args) {

        if (args.max != null && args.sort != null && args.offset != null && args.order != null) {
            return genreMapper.findAllByOffsetAndMaxAndSortAndOrder(
                    args.offset,
                    args.max,
                    args.sort,
                    args.order)
        }

        if (args.max != null && args.offset!= null && (args.sort == null || args.order == null)) {
            return genreMapper.findAllByOffsetAndMax(args.offset, args.max)
        }

        if ((args.max == null || args.offset == null) && args.sort != null && args.order !=null) {
            return genreMapper.findAllBySortAndOrder(args.sort, args.order)
        }

        genreMapper.findAll()
    }

    @Override
    int update(long id, @NonNull @NotBlank String name) {
        genreMapper.update(id, name)
        -1
    }
}

Controller

Micronaut validation is built on the standard framework – JSR 380, also known as Bean Validation 2.0. Micronaut Validation has built-in support for validation of beans that are annotated with jakarta.validation annotations.

To use Micronaut Validation, you need the following dependencies:

build.gradle
compileOnly("io.micronaut.validation:micronaut-validation-processor")
implementation("io.micronaut.validation:micronaut-validation")

Alternatively, you can use Micronaut Hibernate Validator, which uses Hibernate Validator; a reference implementation of the validation API.

Create two classes to encapsulate Save and Update operations:

groovy/src/main/groovy/example/micronaut/genre/GenreSaveCommand.groovy
package example.micronaut.genre

import groovy.transform.CompileStatic
import io.micronaut.core.annotation.NonNull
import io.micronaut.serde.annotation.Serdeable

import jakarta.validation.constraints.NotBlank

@CompileStatic
@Serdeable
class GenreSaveCommand {

    @NotBlank
    @NonNull
    String name

    GenreSaveCommand(@NonNull @NotBlank String name) {
        this.name = name
    }
}
groovy/src/main/groovy/example/micronaut/genre/GenreUpdateCommand.groovy
package example.micronaut.genre

import groovy.transform.CompileStatic
import io.micronaut.core.annotation.NonNull
import io.micronaut.serde.annotation.Serdeable

import jakarta.validation.constraints.NotBlank

@CompileStatic
@Serdeable
class GenreUpdateCommand {

    long id

    @NotBlank
    @NonNull
    String name

    GenreUpdateCommand(long id, @NonNull @NotBlank String name) {
        this.id = id
        this.name = name
    }
}

Create a POJO to encapsulate sorting and pagination:

groovy/src/main/groovy/example/micronaut/ListingArguments.groovy
package example.micronaut

import io.micronaut.core.annotation.NonNull
import io.micronaut.core.annotation.Nullable
import io.micronaut.serde.annotation.Serdeable
import io.micronaut.http.uri.UriBuilder

import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.Positive
import jakarta.validation.constraints.PositiveOrZero
import groovy.transform.CompileStatic

@CompileStatic
@Serdeable
class ListingArguments {

    @PositiveOrZero
    private Integer offset = 0

    @Nullable
    @Positive
    private Integer max

    @Nullable
    @Pattern(regexp = "id|name")
    private String sort

    @Pattern(regexp = "asc|ASC|desc|DESC")
    @Nullable
    private String order

    ListingArguments(Integer offset, @Nullable Integer max, @Nullable String sort, @Nullable String order) {
        this.offset = offset
        this.max = max
        this.sort = sort
        this.order = order
    }

    Integer getOffset() {
        offset
    }

    void setOffset(@Nullable Integer offset) {
        this.offset = offset
    }

    Integer getMax() {
        max
    }

    void setMax(@Nullable Integer max) {
        this.max = max
    }

    String getSort() {
        sort
    }

    void setSort(@Nullable String sort) {
        this.sort = sort
    }

    String getOrder() {
        order
    }

    void setOrder(@Nullable String order) {
        this.order = order
    }

    @NonNull
    static Builder builder() {
        return new Builder()
    }

    URI of(UriBuilder uriBuilder) {
        if (max != null) {
            uriBuilder.queryParam("max", max);
        }
        if (order != null) {
            uriBuilder.queryParam("order", order);
        }
        if (offset != null) {
            uriBuilder.queryParam("offset", offset);
        }
        if (sort != null) {
            uriBuilder.queryParam("sort", sort);
        }
        uriBuilder.build()
    }

    static final class Builder {
        private Integer offset

        @Nullable
        private Integer max

        @Nullable
        private String sort

        @Nullable
        private String order

        private Builder() {
        }

        @NonNull
        Builder max(int max) {
            this.max = max
            this
        }

        @NonNull
        Builder sort(String sort) {
            this.sort = sort
            this
        }

        @NonNull
        Builder order(String order) {
            this.order = order
            this
        }

        @NonNull
        Builder offset(int offset) {
            this.offset = offset
            this
        }

        @NonNull
        ListingArguments build() {
            new ListingArguments(Optional.ofNullable(offset).orElse(0), max, sort, order)
        }
    }
}

Create a ConfigurationProperties class to encapsulate the configuration of the default max value.

groovy/src/main/groovy/example/micronaut/ApplicationConfiguration.groovy
package example.micronaut

interface ApplicationConfiguration {

    int getMax()
}
groovy/src/main/groovy/example/micronaut/ApplicationConfigurationProperties.groovy
package example.micronaut

import groovy.transform.CompileStatic
import io.micronaut.context.annotation.ConfigurationProperties

@CompileStatic
@ConfigurationProperties("application") // 
class ApplicationConfigurationProperties implements ApplicationConfiguration {

    private final int DEFAULT_MAX = 10

    int max = DEFAULT_MAX
}

Create GenreController, a controller that exposes a resource with the common CRUD operations:

groovy/src/main/groovy/example/micronaut/GenreController.groovy

Database Migration with Flyway

We need a way to create the database schema. For that, we use Micronaut integration with Flyway.

Flyway automates schema changes, significantly simplifying schema management tasks, such as migrating, rolling back, and reproducing in multiple environments.

Add the following snippet to include the necessary dependencies:

build.gradle
implementation("io.micronaut.flyway:micronaut-flyway")

We will enable Flyway in the Micronaut configuration file and configure it to perform migrations on one of the defined data sources.

src/main/resources/application.properties
Note
Configuring multiple data sources is as simple as enabling Flyway for each one. You can also specify directories that will be used for migrating each data source. Review the Micronaut Flyway documentation for additional details.

Flyway migration will be automatically triggered before your Micronaut application starts. Flyway will read migration commands in the resources/db/migration/ directory, execute them if necessary, and verify that the configured data source is consistent with them.

Create the following migration files with the database schema creation:

src/main/resources/db/migration/V1__schema.sql
DROP TABLE IF EXISTS GENRE;
DROP TABLE IF EXISTS BOOK;

CREATE TABLE GENRE (
  id    BIGINT AUTO_INCREMENT PRIMARY KEY NOT NULL,
  name VARCHAR(255)              NOT NULL UNIQUE
);

CREATE TABLE BOOK (
  id    BIGINT AUTO_INCREMENT PRIMARY KEY NOT NULL,
  name VARCHAR(255)              NOT NULL,
  isbn VARCHAR(255)              NOT NULL,
  genre_id BIGINT,
    constraint FKM1T3YVW5I7OLWDF32CWUUL7TA
    foreign key (GENRE_ID) references GENRE
);

During application startup, Flyway will execute the SQL file and create the schema needed for the application.

Tests

Create a JUnit test to verify the CRUD operations:

groovy/src/test/groovy/example/micronaut/GenreControllerSpec.groovy

Run the tests:

./gradlew test

Running the App

Running the Application

To run the application, use the ./gradlew run command, which starts the application on port 8080.

We can use curl to check that everything works as expected:

curl http://localhost:8080/genres/list
[]
curl -X POST -d '{"name":"Sci-fi"}' -H "Content-Type: application/json" http://localhost:8080/genres
{"id":1,"name":"Sci-fi"}
curl -X POST -d '{"name":"Science"}' -H "Content-Type: application/json" http://localhost:8080/genres
{"id":2,"name":"Science"}
curl http://localhost:8080/genres/list
[{"id":1,"name":"Sci-fi"},{"id":2,"name":"Science"}]
curl -X DELETE http://localhost:8080/genres/1
curl http://localhost:8080/genres/list
[{"id":2,"name":"Science"}]

Next Steps

Read more about the Configurations for Data Access section and Flyway support in the Micronaut Framework documentation.

Help with the Micronaut Framework

The Micronaut Foundation sponsored the creation of this Guide. A variety of consulting and support services are available.

License

Note
All guides are released with an Apache License 2.0 for the code and a Creative Commons Attribution 4.0 license for the writing and media (images).