Creating a native image with Micronaut 2 and PostgreSQL
With the recent 2.0.0 GA, we are able to use Micronaut with a complete library toolkit including Postgres, Flyway, TestContainers, and GraalVM with minimal effort and impressive performance.
Context
This story continues with the previous article, which by using an in-memory database (H2) we created a REST API to manages users and their messages.
Particularly, we are going to add to support for a relational database (Postgres), a migration system (Flyway), and a dockerized database to test our application in the same way that it works for the development and production environments.
Finally, thanks to the recent improvements in Micronaut and GraalVM, we are able to create a native image for this project and see the results in terms of speed and memory usage.
PostgreSQL
In addition to the Micronaut Data and JPA dependencies, we need the Postgres driver on our build.gradle:
runtimeOnly("org.postgresql:postgresql")
The configuration of our local datasource should be enough to establish connections with:
datasources:
default:
url: jdbc:postgresql://localhost:5432/postgres
driverClassName: org.postgresql.Driver
username: postgres
password: 'sa'
schema-generate: CREATE_DROP
dialect: POSTGRESjpa:
default:
properties:
hibernate:
hbm2ddl:
auto: validate
Next step is to decorate our entities with the necessary annotations, especially the relationship between them:
@Entity(name="USER_DATA")
public class User {
@Id
@GeneratedValue
@Column(name="ID")
private Long id;
@Column(name="USERNAME", nullable = false, unique = true)
@Size(max = 50)
private String username;
@Column(name="PASSWORD")
@Size(max = 50)
private String password;
@Builder.Default
@Column(name="USER_ROLE")
@Size(max = 10)
private String role = "VIEW";
@Column(name="TOKEN", unique = true)
@Size(max = 100)
private String token;
}@Entity(name="MESSAGE")
public class Message {
@Id
@GeneratedValue
@Column(name="ID")
private Long id;
@Column(name="CONTENT")
@Size(max = 250)
private String content;
@Column(name="CREATION_DATE")
private Instant creationDate;
@ManyToOne
@JoinColumn(name = "USER_REF")
private User userRef;
}
Finally, a quick way to start working with a local database is using docker:
$ docker run -d --name local_postgres -v my_dbdata:/var/lib/postgresql/data -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=sa -e POSTGRES_DB=postgres -d postgres:latest
Flyway
A nice way to work with our relational database is by using a migration system, like Flyway, since this kind of tool allows us to declare our DDL schema and apply our SQL scripts.
Short examples of this in this project:
-- V1.0__initial_schema.sql
CREATE SEQUENCE HIBERNATE_SEQUENCE START 4;
CREATE TABLE USER_DATA (
ID BIGINT NOT NULL,
PASSWORD VARCHAR(50),
USER_ROLE VARCHAR(10),
USERNAME VARCHAR(50),
TOKEN VARCHAR(100),
CONSTRAINT CONSTRAINT_UD PRIMARY KEY (ID)
);
CREATE TABLE MESSAGE (
ID BIGINT NOT NULL,
CONTENT VARCHAR(250),
CREATION_DATE TIMESTAMP,
USER_REF BIGINT REFERENCES USER_DATA(id),
CONSTRAINT CONSTRAINT_MS PRIMARY KEY (ID)
);
-- V1.1__initial_data.sql
INSERT INTO public.user_data (id, "password", user_role, username, "token") VALUES(0, 'password1', 'VIEW', 'user1', null);
INSERT INTO public.user_data (id, "password", user_role, username, "token") VALUES(1, 'password2', 'VIEW', 'user2', null);
INSERT INTO public.user_data (id, "password", user_role, username, "token") VALUES(2, 'password3', 'VIEW', 'user3', null);
INSERT INTO public.message (id, "content", creation_date, user_ref) VALUES(0, 'My name is user1', '2020-06-07 21:03:41.889', 0);
INSERT INTO public.message (id, "content", creation_date, user_ref) VALUES(1, 'My name is user2', '2020-06-07 21:03:41.889', 1);
INSERT INTO public.message (id, "content", creation_date, user_ref) VALUES(2, 'My name is user3', '2020-06-07 21:03:41.889', 2);
Lastly, a simple configuration is needed to fetch these migration files hosted into the Flyway default folder (/src/main/resources/db/migration):
flyway:
datasources:
default:
locations: classpath:db/migration
TestContainers
Although we could continue using H2 as our database for testing, independently on our relation database in production, easily we can face some compatibility problems (ex. types) or the impossibility of using specific features.
To overcome this drawback, Testcontainers presents an elegant and transparent solution allowing us to use realistic integration tests. This is particularly important if we are using a high performant framework like Micronaut, where we can afford a lot of e2e integration tests due to the reduced startup time.
In order to enable this feature, we need first to include the dependencies in our build.gradle file:
testImplementation(platform(
"org.testcontainers:testcontainers-bom:1.14.3"))
testRuntimeOnly("org.testcontainers:postgresql")
And the configuration to connect with our dockerized Postgres instance into the application-test.yml:
datasources:
default:
url: jdbc:tc:postgresql://localhost/test
driverClassName: org.testcontainers.jdbc.ContainerDatabaseDriver
username: test
password: test
Optionally, and depending on the isolation degree of our tests, we can choose if the image should be reused in all the tests, or restart it in each of them in the same configuration file:
testcontainers:
reuse:
enable: false
Test Results
Before starting checking our final project, let’s improve the test results by adding a Gradle plugin to for printing beautiful logs on the terminal and configuring it as we like (ex. mocha style):
plugins {
(...)
id 'com.adarshr.test-logger' version '2.0.0'
}(...)test {
useJUnitPlatform()
testlogger {
theme 'mocha'
showExceptions true
showStackTraces true
showFullStackTraces false
showCauses true
slowThreshold 2000
showSummary true
showSimpleNames false
showPassed true
showSkipped true
showFailed true
showStandardStreams false
showPassedStandardStreams true
showSkippedStandardStreams true
showFailedStandardStreams true
}
}
Alright, now it is time to test our application and see if our changes are performing as expected:
$ ./gradlew tests> Task :testmn.data.pg.auth.LoginAuthenticationSpec✔ Verify unauthenticated access does not work
✔ Try to login with a non existing user and fail
✔ Try to login with an existing user but wrong password and fail
✔ Login with an existing user correctly
✔ Login with a forbidden role obtaining access deniedmn.data.pg.auth.OauthAccessTokenSpec✔ Verify tokens are working
✔ Trying to refresh with wrong tokens returning errorsmn.data.pg.controller.SignUpControllerSpec✔ Sign-up providing wrong payload with illegal id
✔ Sign-up with a non existing user correctly
✔ Login with the new user correctlymn.data.pg.controller.MessageControllerSpec✔ Login with an existing user correctly
✔ Retrieve messages from one validate user correctly
✔ Create new message from one validate user correctlymn.data.pg.services.MessageServiceSpec✔ Checking last message My name is user1 from user user1
✔ Checking last message My name is user2 from user user2
✔ Checking last message My name is user3 from user user3
✔ Writing message new message for user1 for user user1
✔ Writing message other message for user1 for user user1
✔ Writing message important message of user2 for user user2
✔ Writing message different message of user2 for user user2
✔ Writing message hello lorep itsum user3 for user user3
✔ Writing message world lorep itsum user3 for user user3
✔ Writing message padding lorep itsum user3 for user user3
✔ Writing message content lorep itsum user3 for user user324 passing (35.2s)BUILD SUCCESSFUL in 43s
4 actionable tasks: 4 executed
Another interesting observation is how the TestContainers and Flyway are starting and the pretty low time penalty that we are paying here:
Particularly, in this case, migrations are applied in 0.113 seconds, but of course, only a few SQL statements are executed. On the other hand, Postgres dockerized instance started in 7.854 seconds, which is really good.
GraalVM
Thanks to the latest improvements in the 2.0.0 GA and following the official guide we can achieve in this kind of project the following speed up:
$ ./gradlew run
15:02:52.715 [main] INFO io.micronaut.runtime.Micronaut — Startup completed in 2612ms. Server Running: http://localhost:8080$ ./mn-data-pg
15:03:17.986 [main] INFO io.micronaut.runtime.Micronaut — Startup completed in 206ms. Server Running: http://localhost:8080
As a quick guide, these dependencies that we need to include :
compileOnly("org.graalvm.nativeimage:svm")
annotationProcessor("io.micronaut:micronaut-graal")
This is the configuration file /resources/META-INF/native-image/mn.data.pg/mn-data-pg-application/native-image.properties
Args = -H:Name=mn-data-pg \
-H:Class=mn.data.pg.Application
And, these are the commands that you need to install the tools and finally build this native image:
$ sdk install java 20.1.0.r8-grl
$ sdk use java 20.1.0.r8-grl
$ gu install native-image
$ ./gradlew assemble
$ native-image --no-server -cp build/libs/mn-data-pg-*-all.jar
Alternatively, we can bake our native image into a docker container:
$ ./gradlew assemble
$ docker build . -t mn-data-pg
$ docker run -p 8080:8080 mn-data-pg
Conclusions
Even in the times of microservices and cloud systems, dealing with a traditional SQL database still fun, and thanks to Micronaut and GraalVM now we are able to do it faster than ever.
As we have seen, thanks to the improved support for Native Images, it is no longer necessary to provide additional GraalVM related configuration to connect to databases (JDBC / Hibernate / JPA), the support for the Postgres (as well as other databases) driver is automatic, and we have native support for Flyway migrations.
If you want to try it, this project repository can be found here: https://github.com/rmondejar/micronaut-postgres-example
See you next time!