mn create-app example.micronaut.micronautguide --build=maven --lang=groovy
Access a database with MyBatis
Learn how to access a database with MyBatis using the Micronaut framework.
Authors: Iván López, Sergio del Amo
Micronaut Version: 4.6.3
1. Getting Started
Learn how to access a database with MyBatis using the Micronaut framework.
2. 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_HOME
configured appropriately
3. 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
4. Writing the Application
Create an application using the Micronaut Command Line Interface or with Micronaut Launch.
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
.
4.1. Configure Data Source and JPA
Add the following snippet to include the necessary dependencies:
<dependency> (1)
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
<scope>compile</scope>
</dependency>
<dependency> (2)
<groupId>io.micronaut.sql</groupId>
<artifactId>micronaut-jdbc-hikari</artifactId>
<scope>compile</scope>
</dependency>
<dependency> (3)
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
1 | Add MyBatis dependency. |
2 | Configures SQL DataSource instances using Hikari Connection Pool. |
3 | Add dependency to in-memory H2 Database. |
Define the data source in src/main/resources/application.yml
.
Unresolved directive in micronaut-data-access-mybatis-maven-groovy.adoc - include::build/code/micronaut-data-access-mybatis/micronaut-data-access-mybatis-maven-groovy/src/main/resources/application.yml[tag=datasource]
4.2. MyBatis configuration
As there is no out-of-the-box support yet in the Micronaut framework for MyBatis, it is necessary to manually wire SqlSessionFactory
.
Create the following @Factory class:
package example.micronaut
import groovy.transform.CompileStatic
import io.micronaut.context.annotation.Factory
import jakarta.inject.Singleton
import org.apache.ibatis.mapping.Environment
import org.apache.ibatis.session.Configuration
import org.apache.ibatis.session.SqlSessionFactory
import org.apache.ibatis.session.SqlSessionFactoryBuilder
import org.apache.ibatis.transaction.TransactionFactory
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory
import javax.sql.DataSource
@CompileStatic
@Factory (1)
class MybatisFactory {
private final DataSource dataSource (2)
MybatisFactory(DataSource dataSource) {
this.dataSource = dataSource (2)
}
@Singleton (3)
SqlSessionFactory sqlSessionFactory() {
TransactionFactory transactionFactory = new JdbcTransactionFactory()
Environment environment = new Environment('dev', transactionFactory, dataSource) (4)
Configuration configuration = new Configuration(environment)
configuration.addMappers('example.micronaut') (5)
new SqlSessionFactoryBuilder().build(configuration) (6)
}
}
1 | Annotate the class with @Factory . |
2 | Use constructor injection to inject a bean of type DataSource . |
3 | Define a @Bean of type SqlSessionFactory . |
4 | Use the dataSource to create a new MyBatis environment. |
5 | Define the package to scan for mappers. |
6 | Create a new SqlSessionFactory bean. |
4.3. Domain
Create the domain entities:
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}"
}
}
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}"
}
}
4.4. 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.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:
package example.micronaut.genre
import example.micronaut.domain.Genre
import groovy.transform.CompileStatic
import jakarta.inject.Singleton
import org.apache.ibatis.session.SqlSession
import org.apache.ibatis.session.SqlSessionFactory
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.Positive
import jakarta.validation.constraints.PositiveOrZero
@CompileStatic
@Singleton (1)
class GenreMapperImpl implements GenreMapper {
private final SqlSessionFactory sqlSessionFactory (2)
GenreMapperImpl(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory (2)
}
@Override
Genre findById(long id) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) { (3)
return getGenreMapper(sqlSession).findById(id) (5)
}
}
@Override
void save(Genre genre) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
getGenreMapper(sqlSession).save(genre)
sqlSession.commit() (6)
}
}
@Override
void deleteById(long id) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
getGenreMapper(sqlSession).deleteById(id)
sqlSession.commit()
}
}
@Override
void update(long id, String name) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
getGenreMapper(sqlSession).update(id, name)
sqlSession.commit()
}
}
@Override
List<Genre> findAll() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return getGenreMapper(sqlSession).findAll()
}
}
@Override
List<Genre> findAllBySortAndOrder(@NotNull @Pattern(regexp = 'id|name') String sort,
@NotNull @Pattern(regexp = 'asc|ASC|desc|DESC') String order) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return getGenreMapper(sqlSession).findAllBySortAndOrder(sort, order)
}
}
@Override
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) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return getGenreMapper(sqlSession).findAllByOffsetAndMaxAndSortAndOrder(offset, max, sort, order)
}
}
@Override
List<Genre> findAllByOffsetAndMax(@PositiveOrZero int offset,
@Positive int max) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return getGenreMapper(sqlSession).findAllByOffsetAndMax(offset, max)
}
}
private GenreMapper getGenreMapper(SqlSession sqlSession) {
sqlSession.getMapper(GenreMapper) (4)
}
}
1 | Use jakarta.inject.Singleton to designate a class as a singleton. |
2 | Easily inject the SqlSessionFactory bean created by the @Factory . |
3 | Use try-with-resources to automatically close the SQL session. |
4 | Get MyBatis mapper implementation for the interface. |
5 | Execute the desired method using the mapper. This will trigger the SQL query. |
6 | In a database write access, commit the transaction. |
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 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
:
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 (1)
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
}
}
4.5. 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:
<dependency>
<groupId>io.micronaut.validation</groupId>
<artifactId>micronaut-validation-processor</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut.validation</groupId>
<artifactId>micronaut-validation</artifactId>
<scope>compile</scope>
</dependency>
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 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
}
}
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:
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.
package example.micronaut
interface ApplicationConfiguration {
int getMax()
}
package example.micronaut
import groovy.transform.CompileStatic
import io.micronaut.context.annotation.ConfigurationProperties
@CompileStatic
@ConfigurationProperties("application") (1)
class ApplicationConfigurationProperties implements ApplicationConfiguration {
private final int DEFAULT_MAX = 10
int max = DEFAULT_MAX
}
Create GenreController
, a controller which exposes a resource with the common CRUD operations:
package example.micronaut
import example.micronaut.domain.Genre
import example.micronaut.genre.GenreRepository
import example.micronaut.genre.GenreSaveCommand
import example.micronaut.genre.GenreUpdateCommand
import groovy.transform.CompileStatic
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Delete
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import io.micronaut.http.annotation.Put
import jakarta.validation.Valid
import static io.micronaut.http.HttpHeaders.LOCATION
@CompileStatic
@Controller('/genres') (1)
class GenreController {
private final GenreRepository genreRepository
GenreController(GenreRepository genreRepository) { (2)
this.genreRepository = genreRepository
}
@Get('/{id}') (3)
Genre show(long id) {
genreRepository.findById(id).orElse(null) (4)
}
@Put (5)
HttpResponse<?> update(@Body @Valid GenreUpdateCommand command) { (6)
genreRepository.update(command.id, command.name)
HttpResponse
.noContent()
.header(LOCATION, location(command.id).path) (7)
}
@Get(value = '/list{?args*}') (8)
List<Genre> list(@Valid ListingArguments args) {
genreRepository.findAll(args)
}
@Post (9)
HttpResponse<Genre> save(@Body @Valid GenreSaveCommand cmd) {
Genre genre = genreRepository.save(cmd.name)
HttpResponse
.created(genre)
.headers(headers -> headers.location(location(genre.id)))
}
@Delete('/{id}') (10)
HttpResponse<?> delete(Long id) {
genreRepository.deleteById(id)
HttpResponse.noContent()
}
private URI location(Long id) {
URI.create('/genres/' + id)
}
}
1 | The class is defined as a controller with the @Controller annotation mapped to the path /genres . |
2 | Constructor injection. |
3 | Maps a GET request to /genres/{id} which attempts to show a genre. This illustrates the use of a URL path variable. |
4 | Returning null when the genre doesn’t exist makes the Micronaut framework respond with 404 (not found). |
5 | Maps a PUT request to /genres which attempts to update a genre. |
6 | Add @Valid to any method parameter which requires validation. Use a POJO supplied as a JSON payload in the request to populate command. |
7 | It is easy to add custom headers to the response. |
8 | Maps a GET request to /genres which returns a list of genres. This mapping illustrates optional URL parameters. |
9 | Maps a POST request to /genres which attempts to save a genre. |
10 | Maps a DELETE request to /genres/{id} which attempts to remove a genre. This illustrates the use of a URL path variable. |
4.6. 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:
<dependency>
<groupId>io.micronaut.flyway</groupId>
<artifactId>micronaut-flyway</artifactId>
<scope>compile</scope>
</dependency>
We will enable Flyway in the Micronaut configuration file and configure it to perform migrations on one of the defined data sources.
(1)
flyway.datasources.default.enabled=true
1 | Enable Flyway for the default datasource. |
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.
4.7. Tests
Create a JUnit test to verify the CRUD operations:
package example.micronaut
import example.micronaut.domain.Genre
import example.micronaut.genre.GenreSaveCommand
import example.micronaut.genre.GenreUpdateCommand
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.client.BlockingHttpClient
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.http.uri.UriBuilder
import io.micronaut.http.uri.UriTemplate
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import spock.lang.Specification
import static io.micronaut.http.HttpHeaders.LOCATION
import static io.micronaut.http.HttpStatus.CREATED
import static io.micronaut.http.HttpStatus.NO_CONTENT
@MicronautTest (1)
class GenreControllerSpec extends Specification {
@Inject
@Client('/')
HttpClient httpClient (2)
void supplyAnInvalidOrderTriggersValidationFailure() {
when:
client.retrieve(
HttpRequest.GET('/genres/list?order=foo'),
Argument.of(List, Genre))
then:
thrown(HttpClientResponseException)
}
void testFindNonExistingGenreReturns404() {
when:
client.retrieve(HttpRequest.GET('/genres/99'), Argument.of(Genre))
then:
thrown(HttpClientResponseException)
}
void testGenreCrudOperations() {
given:
List<Long> genreIds = []
when:
HttpResponse<?> response = saveGenre('DevOps')
genreIds << entityId(response)
then:
CREATED == response.status
when:
response = saveGenre('Microservices') (3)
then:
CREATED == response.status
when:
Long id = entityId(response)
genreIds.add(id)
Genre genre = show(id)
then:
'Microservices' == genre.name
when:
response = update(id, 'Micro-services')
then:
NO_CONTENT == response.status
when:
genre = show(id)
then:
'Micro-services' == genre.name
when:
List<Genre> genres = listGenres(ListingArguments.builder().build())
then:
2 == genres.size()
when:
genres = listGenres(ListingArguments.builder().max(1).build())
then:
1 == genres.size()
'DevOps' == genres[0].name
when:
genres = listGenres(ListingArguments.builder().max(1).order('desc').sort('name').build())
then:
1 == genres.size()
'Micro-services' == genres[0].name
when:
genres = listGenres(ListingArguments.builder().max(1).offset(10).build())
then:
0 == genres.size()
cleanup:
for (long genreId : genreIds) {
response = delete(genreId)
assert NO_CONTENT == response.status
}
}
private List<Genre> listGenres(ListingArguments args) {
URI uri = args.of(UriBuilder.of('/genres/list'))
HttpRequest<?> request = HttpRequest.GET(uri)
client.retrieve(request, Argument.of(List, Genre)) (4)
}
private Genre show(Long id) {
String uri = UriTemplate.of('/genres/{id}').expand(id: id)
HttpRequest<?> request = HttpRequest.GET(uri)
client.retrieve(request, Genre)
}
private HttpResponse<?> update(Long id, String name) {
HttpRequest<?> request = HttpRequest.PUT('/genres', new GenreUpdateCommand(id, name))
client.exchange(request) (5)
}
private HttpResponse<?> delete(Long id) {
HttpRequest<?> request = HttpRequest.DELETE('/genres/' + id)
client.exchange(request)
}
private Long entityId(HttpResponse<?> response) {
String value = response.header(LOCATION)
if (value == null) {
return null
}
String path = '/genres/'
int index = value.indexOf(path)
if (index != -1) {
return value.substring(index + path.length()) as long
}
null
}
private BlockingHttpClient getClient() {
httpClient.toBlocking()
}
private HttpResponse<?> saveGenre(String genre) {
HttpRequest<?> request = HttpRequest.POST('/genres', new GenreSaveCommand(genre)) (3)
client.exchange(request)
}
}
1 | Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info. |
2 | Inject the HttpClient bean and point it to the embedded server. |
3 | Creating HTTP Requests is easy thanks to the Micronaut framework fluid API. |
4 | If you care just about the object in the response use retrieve . |
5 | Sometimes, receiving just the object is not enough and you need information about the response. In this case, instead of retrieve you should use the exchange method. |
Run the tests:
./mvnw test
4.8. Running the App
5. Running the Application
To run the application, use the ./mvnw mn: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"}]
5.1. Next Steps
Read more about Configurations for Data Access section and Flyway support in the Micronaut framework documentation.
6. Help with the Micronaut Framework
The Micronaut Foundation sponsored the creation of this Guide. A variety of consulting and support services are available.
7. License
All guides are released with an Apache license 2.0 license for the code and a Creative Commons Attribution 4.0 license for the writing and media (images…). |