Access a database with MyBatis
Learn how to access a database with MyBatis using Micronaut Framework.
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:
-
Some time on your hands
-
A decent text editor or IDE (e.g. IntelliJ IDEA)
-
JDK 21 or greater installed with
JAVA_HOMEconfigured appropriately
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.
-
Download and unzip the source
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,graalvm \
--build=gradle \
--lang=kotlin \
--test=junit|
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, validation, and graalvm 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:
Define the data source in 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.DriverMyBatis 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:
Domain
Create the domain entities:
package example.micronaut.domain
import com.fasterxml.jackson.annotation.JsonIgnore
import io.micronaut.serde.annotation.Serdeable
@Serdeable
data class Genre(var name: String) {
var id: Long? = null
@JsonIgnore
var books: Set<Book> = mutableSetOf()
override fun toString() = "Genre{id=$id, name='$name', books=$books}"
}package example.micronaut.domain
import io.micronaut.serde.annotation.Serdeable
@Serdeable
data class Book(
var isbn: String,
var name: String,
var genre: Genre?) {
var id: Long? = null
override fun 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:
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.Pattern
import jakarta.validation.constraints.Positive
import jakarta.validation.constraints.PositiveOrZero
interface GenreMapper {
@Select("select * from genre where id=#{id}")
fun findById(id: Long): Genre?
@Insert("insert into genre(name) values(#{name})")
@Options(useGeneratedKeys = true, keyProperty = "id")
fun save(genre: Genre)
@Delete("delete from genre where id=#{id}")
fun deleteById(id: Long)
@Update("update genre set name=#{name} where id=#{id}")
fun update(@Param("id") id: Long, @Param("name") name: String?)
@Select("select * from genre")
fun findAll(): List<Genre>
@Select("select * from genre order by \${sort} \${order}")
fun findAllBySortAndOrder(@Param("sort") @Pattern(regexp = "id|name") sort: String,
@Param("order") @Pattern(regexp = "asc|ASC|desc|DESC") order: String): List<Genre>
@Select("select * from genre order by \${sort} \${order} limit \${offset}, \${max}")
fun findAllByOffsetAndMaxAndSortAndOrder(@Param("offset") @PositiveOrZero offset: Int,
@Param("max") @Positive max: Int,
@Param("sort") @Pattern(regexp = "id|name") sort: String,
@Param("order") @Pattern(regexp = "asc|ASC|desc|DESC") order: String): List<Genre>
@Select("select * from genre limit \${offset}, \${max}")
fun findAllByOffsetAndMax(@Param("offset") @PositiveOrZero offset: Int,
@Param("max") @Positive max: Int): List<Genre>
}And the implementation:
Create an interface to define the high-level operations exposed to the application:
package example.micronaut.genre
import example.micronaut.ListingArguments
import example.micronaut.domain.Genre
import java.util.Optional
import jakarta.validation.constraints.NotBlank
interface GenreRepository {
fun findById(id: Long): Optional<Genre>
fun save(@NotBlank name: String): Genre
fun deleteById(id: Long)
fun findAll(args: ListingArguments): List<Genre>
fun update(id: Long, @NotBlank name: String): Int
}And the implementation using GenreMapper:
package example.micronaut.genre
import example.micronaut.ListingArguments
import example.micronaut.domain.Genre
import io.micronaut.core.annotation.NonNull
import jakarta.inject.Singleton
import java.util.Optional
import jakarta.validation.constraints.NotBlank
@Singleton //
open class GenreRepositoryImpl(private val genreMapper: GenreMapper) : GenreRepository {
override fun findById(id: Long): Optional<Genre> =
Optional.ofNullable(genreMapper.findById(id))
@NonNull
override fun save(@NotBlank name: String): Genre {
val genre = Genre(name)
genreMapper.save(genre)
return genre
}
override fun deleteById(id: Long) {
findById(id).ifPresent { genreMapper.deleteById(id) }
}
@NonNull
override fun findAll(args: ListingArguments): List<Genre> {
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) {
return genreMapper.findAllByOffsetAndMax(args.offset!!, args.max!!)
}
if (args.sort != null && args.order != null) {
return genreMapper.findAllBySortAndOrder(args.sort!!, args.order!!)
}
return genreMapper.findAll()
}
override fun update(id: Long, @NotBlank name: String): Int {
genreMapper.update(id, name)
return -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:
kapt("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:
package example.micronaut.genre
import io.micronaut.serde.annotation.Serdeable
import jakarta.validation.constraints.NotBlank
@Serdeable
data class GenreSaveCommand(@NotBlank var name: String)package example.micronaut.genre
import io.micronaut.serde.annotation.Serdeable
import jakarta.validation.constraints.NotBlank
@Serdeable
class GenreUpdateCommand(var id: Long, @NotBlank var name: String)Create a POJO to encapsulate sorting and pagination:
package example.micronaut
import io.micronaut.serde.annotation.Serdeable
import io.micronaut.http.uri.UriBuilder
import java.net.URI
import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.Positive
import jakarta.validation.constraints.PositiveOrZero
@Serdeable
class ListingArguments(
@field:PositiveOrZero var offset: Int? = 0,
@field:Positive var max: Int? = null,
@field:Pattern(regexp = "id|name") var sort: String? = null,
@field:Pattern(regexp = "asc|ASC|desc|DESC") var order: String? = null
) {
fun of(uriBuilder: UriBuilder): URI {
max?.let { uriBuilder.queryParam("max", it) }
order?.let { uriBuilder.queryParam("order", it) }
offset?.let { uriBuilder.queryParam("offset", it) }
sort?.let { uriBuilder.queryParam("sort", it) }
return uriBuilder.build()
}
class Builder {
private val args = ListingArguments()
fun max(max: Int): Builder = apply { args.max = max }
fun sort(sort: String?): Builder = apply { args.sort = sort }
fun order(order: String?): Builder = apply { args.order = order }
fun offset(offset: Int): Builder = apply { args.offset = offset }
fun build(): ListingArguments = args
}
companion object {
fun builder(): Builder = Builder()
}
}Create a ConfigurationProperties class to encapsulate the configuration of the default max value.
package example.micronaut
interface ApplicationConfiguration {
val max: Int
}package example.micronaut
import io.micronaut.context.annotation.ConfigurationProperties
@ConfigurationProperties("application") //
class ApplicationConfigurationProperties : ApplicationConfiguration {
private val DEFAULT_MAX = 10
override var max = DEFAULT_MAX
}Create GenreController, a controller that exposes a resource with the common CRUD operations:
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:
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.
|
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:
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:
Run the tests:
./gradlew testRunning 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/1curl http://localhost:8080/genres/list[{"id":2,"name":"Science"}]Generate a Micronaut Application Native Executable with GraalVM
We will use GraalVM, an advanced JDK with ahead-of-time Native Image compilation, to generate a native executable of this Micronaut application.
Compiling Micronaut applications ahead of time with GraalVM significantly improves startup time and reduces the memory footprint of JVM-based applications.
|
Note
|
Only Java and Kotlin projects support using GraalVM’s native-image tool. Groovy relies heavily on reflection, which is only partially supported by GraalVM.
|
GraalVM Installation
sdk install java 25.0.2-graalFor installation on Windows, or for a manual installation on Linux or Mac, see the GraalVM Getting Started documentation.
The previous command installs Oracle GraalVM, which is free to use in production and free to redistribute, at no cost, under the GraalVM Free Terms and Conditions.
Alternatively, you can use the GraalVM Community Edition:
sdk install java 25.0.2-graalceNative Executable Generation
To generate a native executable using Gradle, run:
./gradlew nativeCompileThe native executable is created in build/native/nativeCompile directory and can be run with build/native/nativeCompile/micronautguide.
It is possible to customize the name of the native executable or pass additional parameters to GraalVM:
You can execute the same curl request as before to check that the native executable works.
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). |