Oracle native BOOLEAN with Micronaut Data JDBC

Learn how to map Java boolean properties to Oracle Database 23ai native BOOLEAN columns and query them with Micronaut Data JDBC.

Authors: Radovan Radic

Micronaut Version: 5.2.0

1. Getting Started

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

In this guide, you will build a small task list with Micronaut Data JDBC and Oracle Database. Each task has a completed flag.

You will map that flag to a native Oracle BOOLEAN column and query it with repository methods that Micronaut Data translates to Oracle boolean SQL such as IS TRUE and IS FALSE.

Oracle Database 23.1 and later (including Oracle AI Database 26ai) support the SQL BOOLEAN data type. Earlier releases such as Oracle Database 19c and 21c do not, so Micronaut Data keeps its legacy mapping by default: boolean properties are stored in NUMBER(1) columns and compared with 1 and 0. You opt in to native BOOLEAN by telling Micronaut Data which Oracle version your SQL targets.

2. What you will need

To complete this guide, you will need the following:

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

4. 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 \
    --build=maven \
    --lang=groovy \
    --test=spock
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, and serialization-jackson features.

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.

4.1. Oracle Driver

Add also the Oracle Driver

pom.xml
<dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc11</artifactId>
    <scope>runtime</scope>
</dependency>

4.2. Database Configuration

And the database configuration:

src/main/resources/application.properties
datasources.default.schema-generate=CREATE_DROP
(1)
datasources.default.driver-class-name=oracle.jdbc.OracleDriver
(2)
datasources.default.db-type=oracle
(3)
datasources.default.dialect=ORACLE
1 Use Oracle driver.
2 In order for the database to be properly detected by Micronaut Test Resources.
3 Configure the Oracle dialect.

5. Target Oracle Database 23.1

Micronaut Data uses the target Oracle version in two places, and each has its own setting:

  • Repository SQL is generated when your code is compiled. The target version comes from the build or from the repository annotation.

  • Schema generation runs when the application starts. The target version comes from the datasource configuration.

Configure both. The datasource setting does not change SQL that was already generated for repositories at compilation time, and the compile-time setting does not change the generated DDL.

The target version tells Micronaut Data which SQL features it can use. It does not detect the database version. Use numeric major.minor notation: a major-only value such as 23 means 23.0 and does not enable features that require 23.1.

5.1. Repository SQL

There are two ways to set the target version for repository SQL:

  • For all Oracle repositories of the application, set the micronaut.data.sql.dialect-options.oracle.version option in the build, as shown below. Use this when the whole application runs against Oracle Database 23.1 or later, so the version is configured in one place.

  • For a single repository, set the version member of @JdbcRepository. A repository version takes precedence over the build setting, so you can target an individual repository differently, for example while you migrate a schema.

This guide sets the version on the repository, as you will see in the Repository section, so the generated project works without changes to the build. To target all Oracle repositories instead, configure the build as follows and remove version from the annotation.

The Groovy sources are compiled in the Maven JVM, so pass the option as a system property of that JVM, for example in .mvn/jvm.config:

.mvn/jvm.config
-Dmicronaut.data.sql.dialect-options.oracle.version=23.1

5.2. Schema Generation

Schema generation reads the target version from the datasource configuration:

src/main/resources/application.properties
(1)
datasources.default.dialect-options.version=23.1
1 Generate Oracle 23.1-compatible DDL, so boolean properties are created as BOOLEAN columns instead of NUMBER(1).

5.3. Test Resources

Micronaut Test Resources starts Oracle Database Free, which supports native BOOLEAN, for tests and development:

src/main/resources/application.properties
(1)
test-resources.containers.oracle.image-name=gvenzl/oracle-free
test-resources.containers.oracle.image-tag=latest
test-resources.containers.oracle.startup-timeout=600s
1 Use the full Oracle Database Free image and allow the container enough time to start.

6. Task

Create an entity for a task:

src/main/groovy/example/micronaut/Task.groovy
package example.micronaut

import groovy.transform.CompileStatic
import io.micronaut.core.annotation.Nullable
import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.MappedEntity
import io.micronaut.serde.annotation.Serdeable

@CompileStatic
@Serdeable
@MappedEntity('TASK')
class Task {
    @Id
    @GeneratedValue
    @Nullable
    Long id
    String title
    boolean completed (1)

    Task() {
    }

    Task(@Nullable Long id, String title, boolean completed) {
        this.id = id
        this.title = title
        this.completed = completed
    }
}
1 A boolean property. Micronaut Data maps it to a BOOLEAN column because the datasource targets Oracle 23.1.

Schema generation creates the TASK table, and a TASK_SEQ sequence for the generated IDs:

CREATE TABLE "TASK" (
    "ID" NUMBER(19) NOT NULL PRIMARY KEY,
    "TITLE" VARCHAR(255) NOT NULL,
    "COMPLETED" BOOLEAN NOT NULL
)

Without the datasource target version, the COMPLETED column would be created as NUMBER(1).

7. Repository

Create a repository for tasks:

src/main/groovy/example/micronaut/TaskRepository.groovy
package example.micronaut

import io.micronaut.data.annotation.Id
import io.micronaut.data.jdbc.annotation.JdbcRepository
import io.micronaut.data.model.query.builder.sql.Dialect
import io.micronaut.data.repository.CrudRepository

@JdbcRepository(dialect = Dialect.ORACLE, version = '23.1') (1)
interface TaskRepository extends CrudRepository<Task, Long> {

    List<Task> findByCompletedTrue() (2)

    List<Task> findByCompletedFalse() (3)

    List<Task> findByCompleted(boolean completed) (4)

    long updateCompleted(@Id Long id, boolean completed) (5)
}
1 Micronaut Data generates SQL for Oracle Database 23.1 at compilation time. Without a target version, the repository uses the legacy mapping even when the database supports BOOLEAN. See Repository SQL.
2 The True suffix generates WHERE (task_."COMPLETED" IS TRUE). With the legacy mapping, the condition is task_."COMPLETED" = 1.
3 The False suffix generates WHERE (task_."COMPLETED" IS FALSE).
4 A boolean parameter generates WHERE (task_."COMPLETED" = ?). Micronaut Data binds the value as a JDBC BOOLEAN, instead of the BIT type it uses for the legacy mapping.
5 A partial update that sets the completed flag of one task and returns the number of updated rows.

When a repository has a target version, Micronaut Data reads the database version on first use. If the database is older than the target version, it logs a warning, because Oracle Database 19c or 21c rejects the generated BOOLEAN SQL.

8. Controller

Expose the tasks as HTTP endpoints:

src/main/groovy/example/micronaut/TaskController.groovy
package example.micronaut

import groovy.transform.CompileStatic
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
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.http.exceptions.HttpStatusException
import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn

@CompileStatic
@ExecuteOn(TaskExecutors.BLOCKING) (1)
@Controller('/tasks')
class TaskController {

    private final TaskRepository taskRepository

    TaskController(TaskRepository taskRepository) {
        this.taskRepository = taskRepository
    }

    @Post
    @Status(HttpStatus.CREATED)
    Task create(@Body('title') String title) { (2)
        taskRepository.save(new Task(null, title, false))
    }

    @Get('/open')
    List<Task> open() {
        taskRepository.findByCompletedFalse()
    }

    @Get('/completed')
    List<Task> completed() {
        taskRepository.findByCompletedTrue()
    }

    @Put('/{id}/complete')
    Task complete(Long id) {
        if (taskRepository.updateCompleted(id, true) == 0L) { (3)
            throw new HttpStatusException(HttpStatus.NOT_FOUND, 'Task not found')
        }
        taskRepository.findById(id).orElseThrow()
    }
}
1 Repository calls block while they wait for the database, so run them on the blocking executor instead of the event loop.
2 Bind the title property of the JSON request body. New tasks are not completed.
3 updateCompleted returns 0 when there is no task with the given ID, so respond with 404 Not Found.

9. Test

Add a test for the column type, the repository queries and the HTTP endpoints:

src/test/groovy/example/micronaut/OracleBooleanSpec.groovy
package example.micronaut

import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.BlockingHttpClient
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import io.micronaut.transaction.TransactionOperations
import jakarta.inject.Inject
import spock.lang.Specification

import java.sql.Connection
import java.sql.PreparedStatement
import java.sql.ResultSet

@MicronautTest(transactional = false) (1)
class OracleBooleanSpec extends Specification {

    @Inject
    TaskRepository taskRepository

    @Inject
    TransactionOperations<Connection> transactionOperations

    @Inject
    @Client('/')
    HttpClient httpClient

    void setup() {
        taskRepository.deleteAll()
    }

    void 'completed column is native BOOLEAN'() {
        expect:
        columnDataType('TASK', 'COMPLETED') == 'BOOLEAN' (2)
    }

    void 'queries boolean values'() {
        given:
        Task write = taskRepository.save(new Task(null, 'Write the guide', false))
        Task review = taskRepository.save(new Task(null, 'Review the guide', false))

        expect:
        taskRepository.updateCompleted(write.id, true) == 1L (3)

        taskRepository.findByCompletedTrue()*.title == ['Write the guide'] (4)
        taskRepository.findByCompletedFalse()*.title == ['Review the guide']
        taskRepository.findByCompleted(true)*.title == ['Write the guide'] (5)

        taskRepository.findById(write.id).orElseThrow().completed
        !taskRepository.findById(review.id).orElseThrow().completed
    }

    void 'completes a task over HTTP'() {
        given:
        BlockingHttpClient client = httpClient.toBlocking()

        when:
        Task task = client.retrieve(HttpRequest.POST('/tasks', [title: 'Publish the guide']), Task)

        then:
        !task.completed

        when:
        Task completed = client.retrieve(HttpRequest.PUT("/tasks/${task.id}/complete", ''), Task) (6)

        then:
        completed.completed
        client.retrieve(HttpRequest.GET('/tasks/completed'), Argument.listOf(Task))*.title == ['Publish the guide']
        client.retrieve(HttpRequest.GET('/tasks/open'), Argument.listOf(Task)).isEmpty()
    }

    private String columnDataType(String table, String column) {
        transactionOperations.executeRead { status ->
            String sql = 'SELECT data_type FROM user_tab_columns WHERE table_name = ? AND column_name = ?'
            PreparedStatement statement = status.connection.prepareStatement(sql)
            try {
                statement.setString(1, table)
                statement.setString(2, column)
                ResultSet resultSet = statement.executeQuery()
                try {
                    return resultSet.next() ? resultSet.getString(1) : null
                } finally {
                    resultSet.close()
                }
            } finally {
                statement.close()
            }
        }
    }
}
1 Disable the test transaction, so the repository and HTTP requests commit their own transactions.
2 Oracle reports the BOOLEAN data type for the COMPLETED column in the USER_TAB_COLUMNS data dictionary view.
3 Complete one of the tasks.
4 IS TRUE and IS FALSE conditions find the completed and the open task.
5 A boolean parameter is bound as a JDBC BOOLEAN.
6 Complete a task over HTTP.

10. Testing the Application

To run the tests:

./mvnw test

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

11. Run the Application

Start the application with ./gradlew run or ./mvnw mn:run, and create two tasks:

curl -X POST http://localhost:8080/tasks \
     -H 'Content-Type: application/json' \
     -d '{"title":"Write the guide"}'
curl -X POST http://localhost:8080/tasks \
     -H 'Content-Type: application/json' \
     -d '{"title":"Review the guide"}'

Complete the first task:

curl -X PUT http://localhost:8080/tasks/1/complete

List the completed and the open tasks:

curl http://localhost:8080/tasks/completed
curl http://localhost:8080/tasks/open

The first request returns Write the guide, and the second returns Review the guide.

To see the SQL that Micronaut Data runs, set the logger.levels.io.micronaut.data.query configuration property to DEBUG.

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

12. Next Steps

Read more about Oracle BOOLEAN Support in Micronaut Data and the Oracle SQL data types.

13. License

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