Native Image with Micronaut 3.1 and MySQL

Ruben Mondejar
4 min readOct 14, 2021

Let’s revisit this topic, this time with the improved and straightforward solution that the new version of Micronaut with its third iteration.

Context

This story could be seen as a remake of a previous one, but simplifying the process and using MySQL instead of Postgres.

Therefore, again using Flyway to create our schema for the relational database, that we can start flawlessly though the docker image.

In the tech stack, Java 16 (or Kotlin 1.5) and Spock 2 for coding, and of course, Gradle 7 and TestCointainers to fully support the MySQL Dialect thought our application.

best way to craft our new projects

MySQL

As usual we need the dependencies in our build.gradle file for Micronaut Data, JPA, Hibernate, and JDBC, plus the MySQL Driver:

implementation("io.micronaut.data:micronaut-data-hibernate-jpa")
implementation("io.micronaut.sql:micronaut-hibernate-jpa")
implementation("io.micronaut.sql:micronaut-jdbc-hikari")
runtimeOnly("mysql:mysql-connector-java")

Next step is to configure the datasource and JPA in our application.yml:

datasources:
default:
url: jdbc:mysql://localhost/exampleDB?generateSimpleParameterMetadata=true&zeroDateTimeBehavior=convertToNull&verifyServerCertificate=false&useSSL=false
driverClassName: com.mysql.cj.jdbc.Driver
username: root
password: YOUR_MYSQL_PWD
schema-generate: CREATE_DROP
dialect: MYSQL
jpa:
default:
entity-scan:
packages: 'mn.data.mysql.domain'
properties:
hibernate:
bytecode:
provider: none
hbm2ddl:
auto: validate
show_sql: false

Last step is to decorate our entities with the annotations (javax.persistence and javax.validation) related to the DDL that we defined in the next section:

@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="ID")

private Long id;
@Column(name="NAME", nullable = false, unique = true)
@Size(max = 100)

private String name;
@Column(name="BIRTH_YEAR", nullable = false)
private Integer birthYear;

As a reminder, and comparing this to the previous PostgreSQL story, MySQL doesn’t have sequences for our primary keys, we should rely on the AUTO_INCREMENT and the Generation thought IDENTITY :

@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="ID")

private Long id;
@Column(name="TITLE", unique = true)
@Size(max = 150)

private String title;
@Column(name="PUB_DATE")
private Instant pubDate;
@ManyToOne(targetEntity = Author.class)
@JoinColumn(name = "AUTHOR")

private Author author;

Flyway

As usual to handle our database migrations, Flyway takes our SQL scripts from the configured folder and execute them accordantly to our application executions:

Entity–relationship model for this vanilla example
-- V1.0__initial_schema.sql
CREATE TABLE author
(
ID BIGINT NOT NULL AUTO_INCREMENT,
NAME VARCHAR(100) NOT NULL UNIQUE,
BIRTH_YEAR INT NOT NULL,
CONSTRAINT pk_author PRIMARY KEY (ID)
);
CREATE TABLE book
(
ID BIGINT NOT NULL AUTO_INCREMENT,
TITLE VARCHAR(150) NULL,
PUB_DATE datetime NULL,
AUTHOR BIGINT NULL,
CONSTRAINT pk_book PRIMARY KEY (ID)
);
ALTER TABLE book
ADD CONSTRAINT uc_book_title UNIQUE (TITLE);
ALTER TABLE book
ADD CONSTRAINT FK_BOOK_ON_AUTHOR FOREIGN KEY (AUTHOR) REFERENCES author (ID);

Configuration as usual, except for an unexpected issue that has been fixed by enabling the baseline mechanism:

flyway:
datasources:
default:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration

TestContainers

By removing the typical H2 as our database for testing, enables that our code, tests, and any interaction with our relation database is the same in any of environments, guarantees that when we reach production, incompatibility problems (ex. types) are going to surprise us.

Always good reasons to use TestContainers to implement realistic integration tests in our project. Then, in order to enable this feature, we need the corresponding dependencies for our setup in the build.gradle file:

testImplementation("org.testcontainers:mysql")
testImplementation("org.testcontainers:spock")
testImplementation("org.testcontainers:testcontainers")

And the configuration to connect with our dockerized MySQL instance into the application-test.yml:

datasources:
default:
url: jdbc:tc:mysql:8:///db
driverClassName: org.testcontainers.jdbc.ContainerDatabaseDriver

Finally, in order to speed the testing process up, we should try to set a level of isolation in our tests that allow us to reuse the same image, which will reduce our test time dramatically:

testcontainers:
reuse:
enable: true

To run the tests and verify that we are good at this point:

$ ./gradlew testBUILD SUCCESSFUL in 56s
5 actionable tasks: 3 executed, 2 up-to-date

GraalVM

Thanks to the latest improvements, this going to be pretty straightforward, as you can see in the updated official guide. As a quick guide, the required configuration has been reduce to one dependency and one gradle task:

dependencies {
...

compileOnly("org.graalvm.nativeimage:svm")
}
nativeImage {
args('--verbose')
imageName('mn-data-mysql')
}

And, these are the commands that you need to install the tools and finally build this native image:

$ sdk install java 21.2.0.r16-grl 
$ sdk use java 21.2.0.r16-grl
$ gu install native-image
$ ./gradlew nativeImage

The building time has been decimated a lot:

    classlist:   3,199.14 ms,  0.94 GB
(cap): 3,199.71 ms, 0.94 GB
setup: 5,181.91 ms, 0.94 GB
(clinit): 4,429.36 ms, 6.76 GB
(typeflow): 62,585.25 ms, 6.76 GB
(objects): 34,938.55 ms, 6.76 GB
(features): 4,693.12 ms, 6.76 GB
analysis: 112,287.61 ms, 6.76 GB
universe: 6,905.92 ms, 6.80 GB
(parse): 4,274.53 ms, 6.80 GB
(inline): 27,546.31 ms, 6.81 GB
(compile): 50,030.18 ms, 7.14 GB
compile: 88,289.22 ms, 7.14 GB
image: 15,035.10 ms, 7.04 GB
write: 3,421.70 ms, 7.04 GB
[total]: 234,588.60 ms, 7.04 GB

BUILD SUCCESSFUL in 3m 59s

Finally, we can compare the startup times between both JIT and AOT approaches:

$ ./gradlew run
09:34:34.103 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 2044ms. Server Running: http://localhost:8080
$ ./build/native-image/mn-data-mysql
09:33:08.048 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 151ms. Server Running: http://localhost:8080

Conclusions

Glad to say that every time we revisit this topic, the building process and configuration is much simpler and time wise is much faster.

As a reminder, in this example we were focus in a “in-between” solution with JPA, but Micronaut Data provides all the flavours, from the low level handling SQL or more high level Spring-like and Transactional one, but in the end, all of them fully compatible with GraalVM.

Then, test it out yourself, check out this project from the repository and have fun building your own native image: https://github.com/rmondejar/micronaut-mysql-example

Bonus track: if you are more into Kotlin, or do you want to see the differences, here you have the “translation”: https://github.com/rmondejar/micronaut-mysql-kotlin

See you next time!

--

--

Ruben Mondejar

Director of Engineering at @getuberall . Childhood with puzzles, Lego, and MSX. PhD, Passionate Dev, and Proud Father