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.

1. Getting Started

In this guide, we will create a Python application built with Pyronaut.

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:

  • Some time on your hands

  • GraalPy installed and the Pyronaut CLI available locally

  • Docker installed to run Oracle Database with Micronaut Test Resources.

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 Pyronaut CLI (Command Line Interface) or Pyronaut Launch

pyronaut create example.micronaut.micronautguide --features=data-jdbc,oracle,serialization-jackson

The oracle feature adds the Oracle JDBC driver, com.oracle.database.jdbc:ojdbc11, to the runtime dependencies in pyproject.toml.

4.1. Database Configuration

Configure the datasource:

config/application.toml
[datasources.default]
schema-generate = "CREATE_DROP"
(1)
driver-class-name = "oracle.jdbc.OracleDriver"
(2)
db-type = "oracle"
(3)
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 Pyronaut processes your sources. The target version comes 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

Set the version member of @JdbcRepository on each repository that targets Oracle Database 23.1, as you will see in the Repository section.

Pyronaut passes an annotation processor option from the application configuration only when a processor declares that option as supported. Micronaut Data does not declare the micronaut.data.sql.dialect-options.oracle.version option, so setting it in application.toml does not change the generated repository SQL.

5.2. Schema Generation

Schema generation reads the target version from the datasource configuration:

config/application.toml
(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:

config/application.toml
(1)
[test-resources.containers.oracle]
image-name = "gvenzl/oracle-free"
image-tag = "latest"
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/example/micronaut/task.py
from dataclasses import dataclass
from typing import Annotated

from micronaut.data.annotation import GeneratedValue, Id, MappedEntity
from micronaut.serde.annotation import Serdeable


@Serdeable
@MappedEntity("TASK")
@dataclass
class Task:
    title: str
    completed: bool = False  (1)
    id: Annotated[int | None, Id, GeneratedValue] = None
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/example/micronaut/task_repository.py
from typing import Annotated

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

from .task import Task


@JdbcRepository(dialect=Dialect.ORACLE, version="23.1")  (1)
class TaskRepository(CrudRepository[Task, int]):

    def findByCompletedTrue(self) -> list[Task]: ...  (2)

    def findByCompletedFalse(self) -> list[Task]: ...  (3)

    def findByCompleted(self, completed: bool) -> list[Task]: ...  (4)

    def updateCompleted(self, id: Annotated[int, Id], completed: bool) -> int: ...  (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/example/micronaut/task_controller.py
from typing import Annotated

from micronaut.http import HttpResponse, HttpStatus
from micronaut.http.annotation import Body, Controller, Get, Post, Put, Status
from micronaut.scheduling import TaskExecutors
from micronaut.scheduling.annotation import ExecuteOn

from .task import Task
from .task_repository import TaskRepository


@ExecuteOn(TaskExecutors.BLOCKING)  (1)
@Controller("/tasks")
class TaskController:
    def __init__(self, task_repository: TaskRepository):
        self.task_repository = task_repository

    @Post
    @Status(HttpStatus.CREATED)
    def create(self, title: Annotated[str, Body("title")]) -> Task:  (2)
        return self.task_repository.save(Task(title))

    @Get("/open")
    def open(self) -> list[Task]:
        return self.task_repository.findByCompletedFalse()

    @Get("/completed")
    def completed(self) -> list[Task]:
        return self.task_repository.findByCompletedTrue()

    @Put("/{id}/complete")
    def complete(self, id: int) -> HttpResponse[Task]:
        if self.task_repository.updateCompleted(id, True) == 0:  (3)
            return HttpResponse.notFound()
        return HttpResponse.ok(self.task_repository.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:

tests/example/micronaut/test_oracle_boolean.py
import pytest
import requests
from micronaut.transaction import TransactionOperations
from pyronaut.test import MicronautTest, micronaut_test_fixture

from example.micronaut.task import Task
from example.micronaut.task_repository import TaskRepository


@pytest.fixture
def my_context(request):
    fixture = micronaut_test_fixture(
        request,
        MicronautTest(transactional=False),  (1)
    )
    yield fixture
    fixture.stop()


@pytest.fixture
def repository(my_context):
    repository = my_context[TaskRepository]
    repository.deleteAll()
    return repository


@pytest.fixture
def client(my_context):
    return requests.with_context(my_context)


def test_completed_column_is_native_boolean(my_context):
    assert column_data_type(my_context[TransactionOperations], "TASK", "COMPLETED") == "BOOLEAN"  (2)


def test_queries_boolean_values(repository):
    write = repository.save(Task("Write the guide"))
    review = repository.save(Task("Review the guide"))

    assert repository.updateCompleted(write.id, True) == 1  (3)

    assert titles(repository.findByCompletedTrue()) == ["Write the guide"]  (4)
    assert titles(repository.findByCompletedFalse()) == ["Review the guide"]
    assert titles(repository.findByCompleted(True)) == ["Write the guide"]  (5)

    assert repository.findById(write.id).orElseThrow().completed
    assert not repository.findById(review.id).orElseThrow().completed


def test_completes_task_over_http(repository, client):
    task = client.post("/tasks", json={"title": "Publish the guide"}).json()
    assert task["completed"] is False

    completed = client.put(f"/tasks/{task['id']}/complete").json()  (6)
    assert completed["completed"] is True

    assert [t["title"] for t in client.get("/tasks/completed").json()] == ["Publish the guide"]
    assert client.get("/tasks/open").json() == []


def titles(tasks) -> list[str]:
    return [task.title for task in tasks]


def column_data_type(transaction_operations: TransactionOperations, table: str, column: str) -> str | None:
    def read(status):
        sql = "SELECT data_type FROM user_tab_columns WHERE table_name = ? AND column_name = ?"
        statement = status.getConnection().prepareStatement(sql)
        try:
            statement.setString(1, table)
            statement.setString(2, column)
            result = statement.executeQuery()
            return result.getString(1) if result.next() else None
        finally:
            statement.close()

    return transaction_operations.executeRead(read)
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:

pyronaut install
pyronaut validate-config
pyronaut 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 pyronaut 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).