Kotlin / Gradle

Oracle Lock-Free Reservation with Micronaut Data JDBC

Learn how to map Oracle reservable columns and update them concurrently with Micronaut Data JDBC reservation methods.

Radovan Radic
On this guide
In this section

Getting Started

In this guide, we will create a Micronaut application written in Kotlin.

In this guide, you will build a small account service with Micronaut Data JDBC and Oracle Database lock-free reservations. An account has a balance, and many deposits and withdrawals can run concurrently against the same account. You will map the balance to an Oracle RESERVABLE column with a CHECK constraint, and update it with derived reservation methods.

With lock-free reservation, Oracle does not lock the account row for each update. It journals each reservation, checks it against the column constraints, and applies it when the transaction commits. Concurrent transactions can therefore reserve amounts from the same row without waiting for each other, as long as the constraints hold.

Lock-free reservation requires Oracle Database 23.26.1 or later. Oracle documentation may refer to this release by its 26ai name. Micronaut Data does not detect the database version, so use @Reservable only with a compatible database; otherwise Oracle rejects the generated DDL and DML.

What you will need

To complete this guide, you will need the following:

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.

Writing the Application

Create an application using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app example.micronaut.micronautguide \
    --features=data-jdbc,oracle,serialization-jackson,validation \
    --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 data-jdbc, oracle, serialization-jackson, and validation 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.

Oracle Driver

Add also the Oracle Driver

build.gradle
runtimeOnly("com.oracle.database.jdbc:ojdbc11")

Database Configuration

And the database configuration:

kotlin/src/main/resources/application.properties

Micronaut Test Resources starts Oracle Database Free, which supports lock-free reservation, for tests and development:

kotlin/src/main/resources/application.properties

Account

Create an Account entity:

kotlin/src/main/kotlin/example/micronaut/domain/Account.kt

Schema generation creates the table with the reservable column and its check constraint:

CREATE TABLE "ACCOUNT" (
    "ID" NUMBER(19) NOT NULL PRIMARY KEY,
    "NAME" VARCHAR(255) NOT NULL,
    "BALANCE" NUMBER(19) RESERVABLE CONSTRAINT "CK_ACCOUNT_BALANCE_GE_0" CHECK ("BALANCE" >= 0) NOT NULL
)

Other supported validation annotations are @Positive, @Negative, @NegativeOrZero, @Min, @Max, @DecimalMin, and @DecimalMax. Validation annotations generate constraints only on @Reservable properties.

Repository

Create an Oracle JDBC repository:

kotlin/src/main/kotlin/example/micronaut/AccountRepository.kt

The reservation methods generate updates that change the column by a delta, instead of assigning a new value:

UPDATE "ACCOUNT" SET "BALANCE"=("BALANCE" + ?) WHERE ("ID" = ?)
UPDATE "ACCOUNT" SET "BALANCE"=("BALANCE" - ?) WHERE ("ID" = ?)

Oracle does not allow direct assignments to reservable columns, or a mix of reservable and ordinary columns in one update. Micronaut Data therefore leaves balance out of the generated save and update statements, which only change name.

Note
If all updatable properties of an entity are reservable, Micronaut Data cannot generate save or update operations and reports an error at compilation time. Extend GenericRepository instead and declare insert, finder, delete, and reservation methods explicitly.

Controller

Expose account creation, deposits, and withdrawals:

kotlin/src/main/kotlin/example/micronaut/AccountController.kt

Handle a Constraint Violation

When a reservation would violate a column constraint, Oracle rejects it, and Micronaut Data throws DataIntegrityViolationException. Without a handler, this results in a 500 Internal Server Error response. Report it as a conflict instead:

kotlin/src/main/kotlin/example/micronaut/ReservationExceptionHandler.kt

DataIntegrityViolationException is a database error. It is different from a Bean Validation ConstraintViolationException, which Micronaut throws before any SQL runs when validation of a method argument fails.

Test

Add tests for deposits, withdrawals, and constraint failures, through the repository and over HTTP:

kotlin/src/test/kotlin/example/micronaut/AccountTest.kt

Testing the Application

To run the tests:

./gradlew test

Then open build/reports/tests/test/index.html in a browser to see the results.

When you run the tests, Micronaut Test Resources starts an Oracle Database Free container before the application connects.

Inspect the Generated Schema

To inspect the generated columns and constraints, connect to the database, for example, to the Test Resources container while the application runs. The RESERVABLE_COLUMN column of USER_TAB_COLS shows which columns are reservable:

SELECT column_name, data_type, reservable_column
FROM user_tab_cols
WHERE table_name = 'ACCOUNT'
ORDER BY column_id;

List the generated check constraints:

SELECT constraint_name, search_condition
FROM user_constraints
WHERE table_name = 'ACCOUNT'
  AND constraint_type = 'C'
  AND constraint_name LIKE 'CK_%';

Run the Application

Start the application with ./gradlew run or ./mvnw mn:run, and create an account:

curl -X POST http://localhost:8080/accounts \
     -H 'Content-Type: application/json' \
     -d '{"name":"Checking","balance":100}'

Deposit 25:

curl -X POST 'http://localhost:8080/accounts/1/deposit?amount=25'

The response shows a balance of 125.

Withdraw 50:

curl -X POST 'http://localhost:8080/accounts/1/withdraw?amount=50'

The response shows a balance of 75.

Attempt to withdraw more than the balance:

curl -i -X POST 'http://localhost:8080/accounts/1/withdraw?amount=1000'

The response is 409 Conflict, and the account keeps its previous balance.

Micronaut Test Resources Goals

  • zero-configuration: without adding any configuration, test resources should be spawned and the application configured to use them. Configuration is only required for advanced use cases.

  • classpath isolation: use of test resources shouldn’t leak into your application classpath, nor your test classpath

  • compatible with GraalVM native: if you build a native binary, or run tests in native mode, test resources should be available

  • easy to use: the Micronaut build plugins for Gradle and Maven should handle the complexity of figuring out the dependencies for you

  • extensible: you can implement your own test resources, in case the built-in ones do not cover your use case

  • technology agnostic: while lots of test resources use Testcontainers under the hood, you can use any other technology to create resources

Next Steps

Read more about Oracle Reservable Columns in Micronaut Data and Oracle lock-free reservation.

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).