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.
On this guide
In this section
Getting Started
In this guide, we will create a Micronaut application written in Groovy.
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:
-
Some time on your hands
-
A decent text editor or IDE (e.g. IntelliJ IDEA)
-
JDK 21 or greater installed with
JAVA_HOMEconfigured appropriately -
Docker installed to run Oracle Database with Micronaut Test Resources.
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
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=maven \
--lang=groovy \
--test=spock|
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
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11</artifactId>
<scope>runtime</scope>
</dependency>Database Configuration
And the database configuration:
Micronaut Test Resources starts Oracle Database Free, which supports lock-free reservation, for tests and development:
Account
Create an Account entity:
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:
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:
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:
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:
Testing the Application
To run the tests:
./mvnw testWhen 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). |