Create and Connect a Micronaut Application to an Oracle Database on Amazon Relational Database Service

This guide describes how to create a database application using the Graal Development Kit for Micronaut (GDK). The application presents REST endpoints and stores data in an Oracle Database running on Amazon Relational Database Service (RDS) using Micronaut® Data.

Micronaut Data is a database access toolkit that uses ahead-of-time compilation to precompute queries for repository interfaces that are then executed by a thin, lightweight runtime layer. Micronaut Data supports the following back ends: JPA (Hibernate and Hibernate Reactive); SQL (JDBC, R2DBC); and MongoDB.

Prerequisites

Follow the steps below to create the application from scratch. However, you can also download the completed example:

The application ZIP file will be downloaded in your default downloads directory. Unzip it and proceed to the next steps.

A note regarding your development environment

Consider using Visual Studio Code, which provides native support for developing applications with the Graal Development Kit for Micronaut Extension Pack.

Note: If you use IntelliJ IDEA, enable annotation processing.

Windows platform: The GDK guides are compatible with Gradle only. Maven support is coming soon.

1. Create the Application

Create an application using the GDK Launcher.

  1. Open the GDK Launcher in advanced mode.

  2. Create a new project using the following selections.
    • Project Type: Application (Default)
    • Project Name: aws-odb-demo
    • Base Package: com.example (Default)
    • Clouds: AWS
    • Build Tool: Gradle (Groovy) or Maven
    • Language: Java (Default)
    • Test Framework: JUnit (Default)
    • Java Version: 17 (Default)
    • Micronaut Version: (Default)
    • Cloud Services: Database
    • Features: GraalVM Native Image, Oracle Database Server
    • Sample Code: Yes (Default)
  3. Click Generate Project, then click Download Zip. The GDK Launcher creates an application with the package com.example in a directory named aws-odb-demo. The application ZIP file will be downloaded to your default downloads directory. Unzip it, open it in your code editor, and proceed to the next steps.

Alternatively, use the GDK CLI as follows:

gdk create-app com.example.aws-odb-demo \
 --clouds=aws \
 --services=database \
 --features=graalvm,oracle \
 --build=gradle \
 --jdk=17  \
 --lang=java

Open the micronaut-cli.yml file, you can see what features are packaged with the application:

features: [app-name, data, data-jdbc, flyway, gdk-aws-cloud-app, gdk-aws-database, gdk-bom, gdk-license, graalvm, http-client, java, java-application, jdbc-hikari, junit, logback, maven, maven-enforcer-plugin, micronaut-http-validation, netty-server, oracle, properties, readme, serialization-jackson, shade, static-resources, test-resources, validation]

The GDK Launcher creates a multi-module project with two subprojects: aws for Amazon Web Services, and lib for common code and configuration shared across cloud platforms. You develop the application logic in the lib subproject, and keep the Amazon Web Services-specific configurations in the aws subproject.

1.1. Domain Entity

The GDK Launcher created the sample domain entity in the lib/src/main/java/com/example/domain/Genre.java file:

package com.example.domain;

import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.serde.annotation.Serdeable;

import jakarta.validation.constraints.NotNull;

@Serdeable
@MappedEntity
public class Genre {

    @Id
    @GeneratedValue
    private Long id;

    @NotNull
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Genre{" + "id=" + id + ", name='" + name + "'}";
    }
}

You could use a subset of supported JPA annotations instead by including the following compileOnly scoped dependency: jakarta.persistence:jakarta.persistence-api.

1.2. Repository Interface

A repository interface defines the operations to access the database. Micronaut Data implements the interface at compilation time. A sample repository interface was created for you in /lib/src/main/java/com/example/repository/GenreRepository.java:

package com.example.repository;

import com.example.domain.Genre;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.repository.PageableRepository;

import jakarta.validation.constraints.NotBlank;

import static io.micronaut.data.model.query.builder.sql.Dialect.ORACLE;

@JdbcRepository(dialect = ORACLE) (1)
public interface GenreRepository extends PageableRepository<Genre, Long> { (2)

    Genre save(@NonNull @NotBlank String name);

    long update(@Id long id, @NonNull @NotBlank String name);
}
1 @JdbcRepository with a specific dialect.
2 Genre, the entity to treat as the root entity for the purposes of querying, is established either from the method signature or from the generic type parameter specified to the GenericRepository interface.

The repository extends from PageableRepository. It inherits the hierarchy PageableRepositoryCrudRepositoryGenericRepository.

Repository Description

PageableRepository

A repository that supports pagination. It provides findAll(Pageable) and findAll(Sort).

CrudRepository

A repository interface for performing CRUD (Create, Read, Update, Delete). It provides methods such as findAll(), save(Genre), deleteById(Long), and findById(Long).

GenericRepository

A root interface that features no methods but defines the entity type and ID type as generic arguments.

1.3. Controller

Hibernate Validator is a reference implementation of the Validation API. Micronaut has built-in support for validation of beans that use jakarta.validation annotations. The necessary dependencies are included by default when creating a project.

The GDK Launcher created the main controller that exposes a resource with the common CRUD operations for you in src/main/java/com/example/controller/GenreController.java:

package com.example.controller;

import com.example.domain.Genre;
import com.example.service.GenreService;
import io.micronaut.data.model.Pageable;
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 io.micronaut.http.annotation.Status;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import java.net.URI;
import java.util.List;
import java.util.Optional;

import static io.micronaut.http.HttpHeaders.LOCATION;
import static io.micronaut.http.HttpStatus.NO_CONTENT;

@ExecuteOn(TaskExecutors.IO) (1)
@Controller("/genres") (2)
class GenreController {

    private final GenreService genreService;

    GenreController(GenreService genreService) { (3)
        this.genreService = genreService;
    }

    @Get("/{id}") (4)
    public Optional<Genre> show(Long id) {
        return genreService.findById(id);
    }

    @Put("/{id}/{name}") (5)
    public HttpResponse<?> update(long id, String name) {
        genreService.update(id, name);
        return HttpResponse
                .noContent()
                .header(LOCATION, URI.create("/genres/" + id).getPath());
    }

    @Get("/list") (6)
    public List<Genre> list(@Valid Pageable pageable) {
        return genreService.list(pageable);
    }

    @Post (7)
    public HttpResponse<Genre> save(@Body("name") @NotBlank String name) {
        Genre genre = genreService.save(name);

        return HttpResponse
                .created(genre)
                .headers(headers -> headers.location(URI.create("/genres/" + genre.getId())));
    }

    @Delete("/{id}") (8)
    @Status(NO_CONTENT)
    public void delete(Long id) {
        genreService.delete(id);
    }
}
1 It is critical that any blocking I/O operations (such as fetching the data from the database) are offloaded to a separate thread pool that does not block the event loop.
2 The class is defined as a controller with the @Controller annotation mapped to the path /genres.
3 Uses constructor injection to inject a bean of type GenreRepository.
4 Maps a GET request to /genres/{id}, which attempts to show a genre. This illustrates the use of a URL path variable (id).
5 Maps a PUT request to /genres/{id}/{name}, which attempts to update a genre. This illustrates the use of URL path variables (id and name).
6 Maps a GET request to /genres/list, which returns a list of genres. This mapping illustrates URL parameters being mapped to a single POJO.
7 Maps a POST request to /genres, which attempts to create a new genre.
8 Maps a DELETE request to /genres/{id}, which attempts to remove a genre. This illustrates the use of a URL path variable (id).

1.4. Service

A service contains business logic and facilitates communication between the controller and the repository. The domain is used to communicate between the controller and service layers.

The GDK Launcher created a sample service class, lib/src/main/java/com/example/service/GenreService.java, for you:

package com.example.service;

import com.example.domain.Genre;
import com.example.repository.GenreRepository;
import io.micronaut.data.model.Pageable;
import jakarta.inject.Singleton;

import jakarta.transaction.Transactional;
import java.util.List;
import java.util.Optional;

@Singleton
public class GenreService {

    private final GenreRepository genreRepository;

    GenreService(GenreRepository genreRepository) {
        this.genreRepository = genreRepository;
    }

    public Optional<Genre> findById(Long id) {
        return genreRepository.findById(id);
    }

    @Transactional
    public long update(long id, String name) {
        return genreRepository.update(id, name);
    }

    public List<Genre> list(Pageable pageable) {
        return genreRepository.findAll(pageable).getContent();
    }

    @Transactional
    public Genre save(String name) {
        return genreRepository.save(name);
    }

    @Transactional
    public void delete(long id) {
        genreRepository.deleteById(id);
    }
}

1.5. Tests

The GDK Launcher wrote tests for you, in aws/src/test/java/com/example/GenreControllerTest.java, to verify the CRUD operations:

package com.example;

import com.example.domain.Genre;
import com.example.repository.GenreRepository;
import io.micronaut.core.type.Argument;
import io.micronaut.context.env.Environment;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

import static io.micronaut.http.HttpHeaders.LOCATION;
import static io.micronaut.http.HttpStatus.CREATED;
import static io.micronaut.http.HttpStatus.NOT_FOUND;
import static io.micronaut.http.HttpStatus.NO_CONTENT;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

@MicronautTest
class GenreControllerTest {

    @Inject
    @Client("/")
    HttpClient client;

    @Test
    void testFindNonExistingGenreReturns404() {
        HttpClientResponseException thrown = assertThrows(HttpClientResponseException.class, () -> {
            client.toBlocking().exchange(HttpRequest.GET("/genres/99999"));
        });

        assertNotNull(thrown.getResponse());
        assertEquals(NOT_FOUND, thrown.getStatus());
    }

    @Test
    void testGenreCrudOperations() {

        HttpResponse<?> response = client.toBlocking().exchange(
                HttpRequest.POST("/genres", Collections.singletonMap("name", "DevOps")));
        assertEquals(CREATED, response.getStatus());

        response = client.toBlocking().exchange(
                HttpRequest.POST("/genres", Collections.singletonMap("name", "Microservices")));
        assertEquals(CREATED, response.getStatus());

        Long id = entityId(response);

        Genre genre = client.toBlocking().retrieve(
                HttpRequest.GET("/genres/" + id), Genre.class);
        assertEquals("Microservices", genre.getName());

        response = client.toBlocking().exchange(
                HttpRequest.PUT("/genres/" + id + "/Micro-services", null));
        assertEquals(NO_CONTENT, response.getStatus());

        genre = client.toBlocking().retrieve(
                HttpRequest.GET("/genres/" + id), Genre.class);
        assertEquals("Micro-services", genre.getName());

        List<Genre> genres = client.toBlocking().retrieve(
                HttpRequest.GET("/genres/list"), Argument.listOf(Genre.class));
        assertEquals(2, genres.size());

        genres = client.toBlocking().retrieve(
                HttpRequest.GET("/genres/list?size=1"), Argument.listOf(Genre.class));
        assertEquals(1, genres.size());
        assertEquals("DevOps", genres.get(0).getName());

        genres = client.toBlocking().retrieve(
                HttpRequest.GET("/genres/list?size=1&sort=name,desc"), Argument.listOf(Genre.class));
        assertEquals(1, genres.size());
        assertEquals("Micro-services", genres.get(0).getName());

        genres = client.toBlocking().retrieve(
                HttpRequest.GET("/genres/list?size=1&page=2"), Argument.listOf(Genre.class));
        assertEquals(0, genres.size());

        response = client.toBlocking().exchange(
                HttpRequest.DELETE("/genres/" + id));
        assertEquals(NO_CONTENT, response.getStatus());
    }

    private Long entityId(HttpResponse<?> response) {
        String value = response.header(LOCATION);
        if (value == null) {
            return null;
        }
        String path = "/genres/";
        int index = value.indexOf(path);
        return index == -1 ? null : Long.valueOf(value.substring(index + path.length()));
    }

    @Inject
    GenreRepository genreRepository;

    @AfterEach
    void cleanup() {
        genreRepository.deleteAll();
    }
}

2. Create an Oracle Database on Amazon RDS

You will create an Oracle Database instance with the AWS CLI. See the AWS CLI rds Command Reference to learn more about AWS RDS.

2.1. Create an Administrator Account

Instead of using your AWS root account, use an administrator account. If you do not have one already, see Setting up Your Cloud Accounts.

2.2. Create VPC, Security Group, Subnets, and Subnet Group (Optional)

Define the subnet group name:

export DB_SUBNET_GROUP_NAME=micronaut-guides-mysql-sng

To allow connections to the database from your local computer, create a VPC, and a security group that allows access to the database default port from your current public IP address.

Note: Exposing a database port to the internet is a security risk. This should be done only for development purposes.

You will also create two subnets in different availability zones and a subnet group to associate them.

Some of the following commands use jq, which is a lightweight and flexible command-line JSON processor.

# VPC, internet gateway and route table
export VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specification "ResourceType=vpc,Tags=[{Key=Name,Value=GcnAutomatedTestVPC}]" | jq -r '.Vpc.VpcId')
export IG_ID=$(aws ec2 create-internet-gateway  --tag-specification "ResourceType=internet-gateway,Tags=[{Key=Name,Value=GcnAutomatedTestIG}]" | jq -r '.InternetGateway.InternetGatewayId')
aws ec2 attach-internet-gateway --internet-gateway-id $IG_ID --vpc-id $VPC_ID
aws ec2 modify-vpc-attribute --enable-dns-hostnames --vpc-id $VPC_ID
export RT_ID=$(aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$VPC_ID" --query "RouteTables[].RouteTableId" --output text)
export TMP_RES=$(aws ec2 create-route --route-table-id $RT_ID --destination-cidr-block 0.0.0.0/0 --gateway-id $IG_ID)

# Security group
export SG_ID=$(aws ec2 create-security-group --group-name gcn-test-guides-oracledb-sg --description "Security Group for the Test GCN Oracle DB guide" --vpc-id $VPC_ID | jq -r '.GroupId')
aws ec2 authorize-security-group-ingress --group-id $SG_ID --protocol tcp --port 1521 --cidr 0.0.0.0/0 | jq -r '.SecurityGroupRules[0]'

# Subnets and subnet group
export AZ_0=$(aws ec2 describe-availability-zones --filters "Name=state,Values=available" --query "AvailabilityZones[0].ZoneName" --output text)
export AZ_1=$(aws ec2 describe-availability-zones --filters "Name=state,Values=available" --query "AvailabilityZones[1].ZoneName" --output text)
export SN0_ID=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.0.0/20 --availability-zone $AZ_0 | jq -r '.Subnet.SubnetId')
export SN1_ID=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.16.0/20 --availability-zone $AZ_1 | jq -r '.Subnet.SubnetId')
aws ec2 modify-subnet-attribute --subnet-id $SN0_ID --map-public-ip-on-launch
aws ec2 modify-subnet-attribute --subnet-id $SN1_ID --map-public-ip-on-launch
export DBSubnetGroup=$(aws rds create-db-subnet-group --db-subnet-group-name $DB_SUBNET_GROUP_NAME --db-subnet-group-description "DB subnet group for Testing the GCN  Oracle DB guide" --subnet-ids "$SN0_ID" "$SN1_ID")

2.3. Create an Oracle RDS Instance

Set the following environment variables (you can change the values to any of your choice):

export DB_INSTANCE_NAME=awsodbguidetemp
export ADMIN_PASSWORD=Passw0rd1234!
export DB_NAME=DATABASE
export ADMIN_USERNAME=admin
  1. Run this command to create a MySQL instance in AWS RDS:

    export DBInstanceArn=$(aws rds create-db-instance \
                            --db-name $DB_NAME \
                            --db-instance-identifier $DB_INSTANCE_NAME \
                            --db-instance-class db.m5.large \
                            --engine oracle-se2 \
                            --master-username $ADMIN_USERNAME \
                            --master-user-password $ADMIN_PASSWORD \
                            --allocated-storage 20 \
                            --db-subnet-group-name $DB_SUBNET_GROUP_NAME \
                            --vpc-security-group-ids $SG_ID \
                            --license-model license-included \
                            --publicly-accessible \
                            --output json | jq -r '.DBInstance.DBInstanceArn')
  2. Wait for the instance to become available:

    aws rds wait db-instance-available --db-instance-identifier "$DB_INSTANCE_NAME" --output text
  3. Once the instance is available, set the value of an environment variable (named ORACLEDB_HOST) to represent its hostname:

    export ORACLEDB_HOST=$(aws rds describe-db-instances --query 'DBInstances[?DBInstanceIdentifier==`awsodbguidetemp`].Endpoint.Address' --output text)

2.4. Configure Datasources

Define the datasources environment variables. You can customize the values of the username and password as needed:

export DATASOURCES_DEFAULT_PASSWORD=UserPassw0rd1234
export DATASOURCES_DEFAULT_URL=jdbc:oracle:thin:@$ORACLEDB_HOST:1521/$DB_NAME
export DATASOURCES_DEFAULT_USERNAME=awsuser

2.5. Create a Database and a Database User

Connect to the database using the SqlPlus Client CLI and create a database user:

output=$(sqlplus -s $ADMIN_USERNAME/$ADMIN_PASSWORD@$ORACLEDB_HOST:1521/$DB_NAME @create_user.sql $DATASOURCES_DEFAULT_USERNAME $DATASOURCES_DEFAULT_PASSWORD)
echo "$output"

This command executes the following SQL command, contained in the create_user.sql file:

CREATE USER &1 IDENTIFIED BY &2;
GRANT CONNECT, RESOURCE TO &1;
GRANT UNLIMITED TABLESPACE TO &1;
exit

The placeholder values &1 and &2 in the SQL command are replaced with the values of the environment variables you set earlier, respectively DATASOURCES_DEFAULT_USERNAME and DATASOURCES_DEFAULT_PASSWORD.

3. Configuration

Before you can test or run your application, provide configuration for Flyway and the application’s datasources.

3.1. Configure Flyway

The GDK Launcher included Flyway for database migrations. It uses the Micronaut integration with Flyway that automates schema changes, significantly simplifies schema management tasks, such as migrating, rolling back, and reproducing in multiple environments. The GDK Launcher enables Flyway in the aws/src/main/resources/application.properties file and configures it to perform migrations on the default datasources.

flyway.datasources.default.enabled=true

If you specified Flyway as a project feature in the GDK Launcher the build file includes it as a dependency:

build.gradle

implementation("io.micronaut.flyway:micronaut-flyway")
implementation("org.flywaydb:flyway-mysql")

Note: Flyway migrations are not compatible with the default automatic schema generation that is configured in aws/src/main/resources/application.properties. If schema-generate is active, it will conflict with Flyway. So edit aws/src/main/resources/application.properties and either delete the datasources.default.schema-generate=CREATE_DROP line or change that line to datasources.default.schema-generate=NONE to ensure that only Flyway manages your schema.

Configuring multiple datasources is as simple as enabling Flyway for each one. You can also specify directories that will be used for migrating each datasource. For more information, see Micronaut integration with Flyway.

Flyway migration is automatically triggered before your application starts. Flyway reads migration file(s) in the lib/src/main/resources/db/migration/ directory. The migration file with the database schema, lib/src/main/resources/db/migration/V1__schema.sql, was also created for you by the GDK Launcher.

CREATE TABLE "GENRE" (
   "ID"    NUMBER(19) PRIMARY KEY NOT NULL,
   "NAME"  VARCHAR(255) NOT NULL
);
CREATE SEQUENCE "GENRE_SEQ" MINVALUE 1 START WITH 1 NOCACHE NOCYCLE;

During application startup, Flyway runs the commands in the SQL file and creates the schema needed for the application.

4. Run the Application

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

./gradlew :aws:run

5. Test the Application

Test the application by accessing its REST endpoints.

Run this command to test creating and storing a new Genre in the database:

TEST_RES=$(curl -X "POST" "http://localhost:8080/genres" \
               -H 'Content-Type: application/json; charset=utf-8' \
               -d $'{ "name": "music" }')

echo "Result: $TEST_RES"

You should see a response similar to:

{"id":1,"name":"music"}

Confirm that the new Genre is saved in the database by listing its contents:

TEST_RES=$(curl -s localhost:8080/genres/list)
echo "Result: $TEST_RES"

You should see a response similar to:

[{"id":1,"name":"music"}]

Retrieve a single Genre from the database as follows:

curl -i http://localhost:8080/genres/1

You should see a response similar to:

{"id":1,"name":"music"}

Delete the Genre you added and then list the contents of the database to confirm that it has been deleted:

curl -s -X "DELETE" "http://localhost:8080/genres/$ITEM_ID"
TEST_RES=$(curl -s localhost:8080/genres/list)
echo "Result: $TEST_RES"

6. Generate a Native Executable Using GraalVM

Start by adding the reflect-config.json file in aws/src/main/resources/META-INF/native-image/com/example with the following content:

[
{
  "name":"org.flywaydb.core.api.migration.baseline.BaselineMigrationConfigurationExtension",
  "allDeclaredFields":true,
  "queryAllDeclaredMethods":true,
  "queryAllDeclaredConstructors":true,
  "methods":[{"name":"<init>","parameterTypes":[] }, {"name":"getBaselineMigrationPrefix","parameterTypes":[] }, {"name":"setBaselineMigrationPrefix","parameterTypes":["java.lang.String"] }]
},
{
  "name":"org.flywaydb.core.extensibility.ConfigurationExtension",
  "queryAllDeclaredMethods":true
},
{
  "name":"org.flywaydb.core.extensibility.Plugin",
  "queryAllDeclaredMethods":true
},
{
  "name":"org.flywaydb.core.internal.command.clean.CleanModeConfigurationExtension",
  "allDeclaredFields":true,
  "queryAllDeclaredMethods":true,
  "queryAllDeclaredConstructors":true,
  "methods":[{"name":"<init>","parameterTypes":[] }, {"name":"getClean","parameterTypes":[] }, {"name":"setClean","parameterTypes":["org.flywaydb.core.internal.command.clean.CleanModel"] }]
},
{
  "name":"org.flywaydb.core.internal.command.clean.CleanModel",
  "allDeclaredFields":true,
  "queryAllDeclaredMethods":true,
  "queryAllDeclaredConstructors":true
},
{
  "name":"org.flywaydb.core.internal.command.clean.SchemaModel",
  "allDeclaredFields":true,
  "queryAllDeclaredMethods":true,
  "queryAllDeclaredConstructors":true
},
{
  "name":"org.flywaydb.core.internal.logging.slf4j.Slf4jLogCreator",
  "methods":[{"name":"<init>","parameterTypes":[] }]
},
{
  "name":"org.flywaydb.core.internal.proprietaryStubs.LicensingConfigurationExtensionStub",
  "allDeclaredFields":true
},
{
  "name":"org.flywaydb.database.oracle.OracleConfigurationExtension",
  "allDeclaredFields":true,
  "queryAllDeclaredMethods":true,
  "queryAllDeclaredConstructors":true,
  "methods":[{"name":"<init>","parameterTypes":[] }, {"name":"getKerberosCacheFile","parameterTypes":[] }, {"name":"getSqlplus","parameterTypes":[] }, {"name":"getSqlplusWarn","parameterTypes":[] }, {"name":"getWalletLocation","parameterTypes":[] }, {"name":"setKerberosCacheFile","parameterTypes":["java.lang.String"] }, {"name":"setSqlplus","parameterTypes":["java.lang.Boolean"] }, {"name":"setSqlplusWarn","parameterTypes":["java.lang.Boolean"] }, {"name":"setWalletLocation","parameterTypes":["java.lang.String"] }]
}
]

The GDK supports compiling Java applications ahead-of-time into native executables using GraalVM Native Image. You can use the Gradle plugin for GraalVM Native Image building/Maven plugin for GraalVM Native Image building. Packaged as a native executable, it significantly reduces application startup time and memory footprint.

Prerequisites: Make sure you have installed a GraalVM JDK. The easiest way to get started is with SDKMAN!. For other installation options, visit the Downloads section.

  1. To generate a native executable, use the following command:

    ./gradlew :aws:nativeCompile

    The native executable is created in the aws/build/native/nativeCompile/ directory

  2. You can then run the native executable with the following command:

    aws/build/native/nativeCompile/aws-odb-demo-aws

    You can customize the name of the resulting binary by updating the Maven/Gradle plugin for GraalVM Native Image configuration.

7. Run and Test the Native Executable

Run the native executable, and then perform the same tests as in step 5.

8. Stop Database Instance and Clean Up

Once you are done with this guide, you can stop and/or delete the AWS resources created to avoid incurring unnecessary charges. Run these commands:

export DB_INSTANCE=$(aws rds delete-db-instance --db-instance-identifier $DB_INSTANCE_NAME --skip-final-snapshot --query "DBInstance.DBInstanceIdentifier" --output json)
aws rds wait db-instance-deleted --db-instance-identifier $DB_INSTANCE_NAME
aws ec2 delete-subnet --subnet-id $SN0_ID
aws ec2 delete-subnet --subnet-id $SN1_ID
aws rds delete-db-subnet-group --db-subnet-group-name $DB_SUBNET_GROUP_NAME
aws ec2 delete-security-group --group-id $SG_ID
aws ec2 detach-internet-gateway --internet-gateway-id $IG_ID --vpc-id $VPC_ID
aws ec2 delete-internet-gateway --internet-gateway-id $IG_ID
aws ec2 delete-vpc --vpc-id $VPC_ID

Summary

This guide demonstrated how to use the GDK to create a database application that stores data in an Oracle Database on Amazon RDS using Micronaut Data. You also saw how to package this application into a native executable.