Java + Oracle Database 12c で主要な SQL 型の値を取得するサンプルコード

## 概要

- Oracle Database 12c にてテーブルに主要な SQL データ型のカラムを定義して、Java のプログラムからカラムの値を取得するサンプルコードを示す
- 今回の動作確認環境: Oracle Database 12c Release 2 (12.2.0.1.0) Enterprise Edition (on Docker) + Oracle JDBC Thin driver (ojdbc8.jar) 19.7.0.0 + Java 14 (AdoptOpenJDK 14.0.2) + Gradle 6.6 + macOS Catalina

## サンプルコード

### ファイル一覧

```
├── build.gradle
└── src
    └── main
        └── java
            └── JdbcSample.java
```

### build.gradle

```groovy
plugins {
  id 'application'
  id 'java'
}

sourceCompatibility = JavaVersion.VERSION_14

repositories {
  mavenCentral()
}

dependencies {

  // 実行時に Oracle JDBC Driver を使うだけなら runtimeOnly を指定
  //runtimeOnly 'com.oracle.database.jdbc:ojdbc8:19.7.0.0'

  // 今回は oracle.jdbc.OracleTypes を使うため implementation を指定
  implementation 'com.oracle.database.jdbc:ojdbc8:19.7.0.0'
}

tasks.withType(JavaCompile) {
  // Java 14 のプレビュー機能を使う
  options.compilerArgs += ['--enable-preview']
}

application {
  // Java 14 のプレビュー機能を使う
  applicationDefaultJvmArgs = ['--enable-preview']
  mainClassName = 'JdbcSample'
}
```

### JdbcSample.java

```java
import oracle.jdbc.OracleTypes; // 普通に使うだけなら不要 (今回は Oracle 拡張 JDBC 型の情報を取得するために使用)

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.sql.Types;
import java.util.Arrays;

class JdbcSample {

  public static void main(String[] args) throws Exception {

    // MySQL に接続
    String url = "jdbc:oracle:thin:@//localhost:1521/testdb";
    String user = "javarista";
    String password = "cafebabe";
    Connection con = DriverManager.getConnection(url, user, password);
    Statement stmt = con.createStatement();

    // テーブルを作成
    // いろいろな SQL データ型でカラムを定義
    // (Java 14 プレビュー機能で使えるヒアドキュメントっぽく書けるテキストブロック機能を使う)
    stmt.execute("""
      create table test (
        -- 文字列型
        my_char      CHAR(8 CHAR),        -- 固定長文字列 (基本は最大長 2000 bytes)
        my_varchar2  VARCHAR2(512 CHAR),  -- 可変長文字列 (基本は最大長 4000 bytes)
        my_clob      CLOB,                -- Character Large Object

        -- バイナリ型
        my_raw   RAW(256),  -- 可変長バイナリ (基本は最大長 2000 bytes)
        my_blob  BLOB,      -- Binary Large Object

        -- 真偽値型
        my_number_1  NUMBER(1),  -- 0 は false、それ以外の値は true として解釈される

        -- 整数型
        my_number_38  NUMBER(38),  -- 最大38桁
        my_smallint   SMALLINT,    -- Oracle データ型に変換される ANSI データ型
        my_integer    INTEGER,     -- Oracle データ型に変換される ANSI データ型

        -- 浮動小数点型
        my_binary_float      BINARY_FLOAT,      -- 単精度浮動小数点数 4 bytes
        my_binary_double     BINARY_DOUBLE,     -- 倍精度浮動小数点数 8 bytes
        my_float             FLOAT,             -- Oracle データ型 FLOAT(126) に変換される ANSI データ型
        my_double_precision  DOUBLE PRECISION,  -- Oracle データ型 FLOAT(126) に変換される ANSI データ型
        my_real              REAL,              -- Oracle データ型 FLOAT(63)  に変換される ANSI データ型

        -- 固定小数点型
        my_fixed_point_number  NUMBER(7, 4),   -- 固定小数点数
        my_numeric             NUMERIC(7, 4),  -- Oracle データ型に変換される ANSI データ型
        my_decimal             DECIMAL(7, 4),  -- Oracle データ型に変換される ANSI データ型

        -- 時間型
        my_date                      DATE,                        -- 年月日 + 時分秒
        my_timestamp                 TIMESTAMP(9),                -- 年月日 + 時分秒 + ナノ秒
        my_timestamp_with_time_zone  TIMESTAMP(9) WITH TIME ZONE  -- 年月日 + 時分秒 + ナノ秒 + タイムゾーン
      )""");

    // レコードを追加
    // (Java 14 プレビュー機能で使えるヒアドキュメントっぽく書けるテキストブロック機能を使う)
    stmt.execute("""
      insert into test values (
        -- 文字列型
        'Hello', -- CHAR
        'Hello', -- VARCHAR2
        'Hello', -- CLOB

        -- バイナリ型
        HEXTORAW('CAFEBABE'),  -- RAW
        HEXTORAW('CAFEBABE'),  -- BLOB,

        -- 真偽値型
        1,  -- 0 は false、それ以外の値は true として解釈される

        -- 整数型
        12345678901234567890123456789012345678,  -- NUMBER(38)
        32767,                                   -- SMALLINT
        2147483647,                              -- INTEGER

        -- 浮動小数点型
        123.0001,  -- BINARY_FLOAT
        123.0001,  -- BINARY_DOUBLE
        123.0001,  -- FLOAT
        123.0001,  -- DOUBLE PRECISION
        123.0001,  -- REAL

        -- 固定小数点型
        123.0001,  -- NUMBER(7, 4)
        123.0001,  -- NUMERIC(7, 4)
        123.0001,  -- DECIMAL(7, 4)

        -- 時間型
        TO_DATE('2001-02-03 04:05:06', 'YYYY-MM-DD HH24:MI:SS'),                                      -- DATE
        TO_TIMESTAMP('2001-02-03 04:05:06.999999999', 'YYYY-MM-DD HH24:MI:SS.FF9'),                   -- TIMESTAMP(9)
        TO_TIMESTAMP_TZ('2001-02-03 04:05:06.999999999 +00:00', 'YYYY-MM-DD HH24:MI:SS.FF9 TZH:TZM')  -- TIMESTAMP(9) WITH TIME ZONE
      )""");

    // レコードを取得
    ResultSet rs = stmt.executeQuery("select * from test");
    while (rs.next()) {

      // カラムの JDBC 型や SQL 型に対する Java オブジェクトの型を取得
      System.out.println("カラム名 - JDBC 型 - データベース固有の SQL 型 - Java オブジェクトの型");
      ResultSetMetaData rsmd = rs.getMetaData();
      for (int i = 1; i <= rsmd.getColumnCount(); i++) {
        System.out.println(
          rsmd.getColumnName(i) + " - " +
            getJdbcTypeName(rsmd.getColumnType(i)) + " - " +
            rsmd.getColumnTypeName(i) + " - " +
            rsmd.getColumnClassName(i));
      }
      System.out.println();

      // カラムの値を取得していく
      System.out.println("カラム名 - カラムの値");

      // 文字列型
      System.out.println("my_char=" + rs.getString("my_char"));
      System.out.println("my_varchar2=" + rs.getString("my_varchar2"));
      System.out.println("my_clob=" + rs.getClob("my_clob"));

      // バイナリ型
      System.out.println("my_raw=" + Arrays.toString(rs.getBytes("my_raw")));
      System.out.println("my_blob=" + rs.getBlob("my_blob"));

      // 真偽値型
      System.out.println("my_number_1=" + rs.getBoolean("my_number_1"));

      // 整数型
      System.out.println("my_number_38=" + rs.getBigDecimal("my_number_38"));
      System.out.println("my_smallint=" + rs.getInt("my_smallint"));
      System.out.println("my_integer=" + rs.getInt("my_integer"));

      // 浮動小数点型
      System.out.println("my_binary_float=" + rs.getFloat("my_binary_float"));
      System.out.println("my_binary_double=" + rs.getDouble("my_binary_double"));
      System.out.println("my_float=" + rs.getDouble("my_float"));
      System.out.println("my_double_precision=" + rs.getDouble("my_double_precision"));
      System.out.println("my_real=" + rs.getDouble("my_real"));

      // 固定小数点型
      System.out.println("my_fixed_point_number=" + rs.getBigDecimal("my_fixed_point_number"));
      System.out.println("my_numeric=" + rs.getBigDecimal("my_numeric"));
      System.out.println("my_decimal=" + rs.getBigDecimal("my_decimal"));

      // 時間型
      System.out.println("my_date=" + rs.getTimestamp("my_date"));
      System.out.println("my_timestamp=" + rs.getTimestamp("my_timestamp").toInstant());
      System.out.println("my_timestamp_with_time_zone=" + rs.getTimestamp("my_timestamp_with_time_zone").toInstant());
    }

    stmt.close();
    con.close();
  }

  // JDBC 型の名称を取得する
  private static String getJdbcTypeName(int type) throws IllegalAccessException {
    // Java 標準の JDBC 型から探す
    Field[] fs = Types.class.getDeclaredFields();
    for (Field f : fs) {
      if (type == f.getInt(null)) {
        return f.getName();
      }
    }
    // Oracle 拡張機能の JDBC 型から探す
    fs = OracleTypes.class.getDeclaredFields();
    for (Field f : fs) {
      if (type == f.getInt(null)) {
        return "OracleTypes." + f.getName();
      }
    }
    // 合致する JDBC 型が無かったので type 値を文字列化して返す
    return "" + type;
  }
}
```

## 実行結果

Gradle の run タスクで実行。

```
$ gradle run
Starting a Gradle Daemon (subsequent builds will be faster)

> Task :compileJava
注意:/Users/foo/bar/src/main/java/JdbcSample.javaはプレビュー言語機能を使用します。
注意:詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。

> Task :run
カラム名 - JDBC 型 - データベース固有の SQL 型 - Java オブジェクトの型
MY_CHAR - CHAR - CHAR - java.lang.String
MY_VARCHAR2 - VARCHAR - VARCHAR2 - java.lang.String
MY_CLOB - CLOB - CLOB - oracle.jdbc.OracleClob
MY_RAW - VARBINARY - RAW - [B
MY_BLOB - BLOB - BLOB - oracle.jdbc.OracleBlob
MY_NUMBER_1 - NUMERIC - NUMBER - java.math.BigDecimal
MY_NUMBER_38 - NUMERIC - NUMBER - java.math.BigDecimal
MY_SMALLINT - NUMERIC - NUMBER - java.math.BigDecimal
MY_INTEGER - NUMERIC - NUMBER - java.math.BigDecimal
MY_BINARY_FLOAT - OracleTypes.BINARY_FLOAT - BINARY_FLOAT - java.lang.Float
MY_BINARY_DOUBLE - OracleTypes.BINARY_DOUBLE - BINARY_DOUBLE - java.lang.Double
MY_FLOAT - NUMERIC - NUMBER - java.lang.Double
MY_DOUBLE_PRECISION - NUMERIC - NUMBER - java.lang.Double
MY_REAL - NUMERIC - NUMBER - java.lang.Double
MY_FIXED_POINT_NUMBER - NUMERIC - NUMBER - java.math.BigDecimal
MY_NUMERIC - NUMERIC - NUMBER - java.math.BigDecimal
MY_DECIMAL - NUMERIC - NUMBER - java.math.BigDecimal
MY_DATE - TIMESTAMP - DATE - java.sql.Timestamp
MY_TIMESTAMP - TIMESTAMP - TIMESTAMP - oracle.sql.TIMESTAMP
MY_TIMESTAMP_WITH_TIME_ZONE - OracleTypes.TIMESTAMPTZ - TIMESTAMP WITH TIME ZONE - oracle.sql.TIMESTAMPTZ

カラム名 - カラムの値
my_char=Hello   
my_varchar2=Hello
my_clob=oracle.sql.CLOB@7c711375
my_raw=[-54, -2, -70, -66]
my_blob=oracle.sql.BLOB@3a44431a
my_number_1=true
my_number_38=12345678901234567890123456789012345678
my_smallint=32767
my_integer=2147483647
my_binary_float=123.0001
my_binary_double=123.0001
my_float=123.0001
my_double_precision=123.0001
my_real=123.0001
my_fixed_point_number=123.0001
my_numeric=123.0001
my_decimal=123.0001
my_date=2001-02-03 04:05:06.0
my_timestamp=2001-02-02T19:05:06.999999999Z
my_timestamp_with_time_zone=2001-02-03T04:05:06.999999999Z

BUILD SUCCESSFUL in 16s
2 actionable tasks: 2 executed
```

## Oracle Database の真偽値型

[Oracle Database JDBC開発者ガイド, 12cリリース2 \(12\.2\) \- Oracleデータへのアクセスと操作](https://docs.oracle.com/cd/E82638_01/jjdbc/accessing-and-manipulating-Oracle-data.html#GUID-EE0C380B-AB3E-4D1C-B02F-E3E599C72F91)

> BOOLEANデータベース型が存在しないため、getBooleanを使用すると必ずデータ型変換が実行されます。getBooleanメソッドは数値用の列に対してのみサポートされます。このような列に対してgetBooleanが適用されると、0(ゼロ)値はfalseとして、それ以外の値はtrueとして解釈されます。別の種類の列に適用された場合は、getBooleanは例外java.lang.NumberFormatExceptionを戻します。

## 参考資料

- [JDBC API 入門 \- SQL と Java の型のマッピング](https://docs.oracle.com/javase/jp/6/technotes/guides/jdbc/getstart/mapping.html)
- [java\.sql \(Java SE 14 & JDK 14\)](https://docs.oracle.com/javase/jp/14/docs/api/java.sql/module-summary.html)
- [Oracle Database概要, 12cリリース2 \(12\.2\) \- 表と表クラスタ \- Oracleデータ型](https://docs.oracle.com/cd/E82638_01/cncpt/tables-and-table-clusters.html#GUID-A8F3420D-093C-449F-87E4-6C3DDFA8BCFF)
- [Oracle Database SQL言語リファレンス, 12cリリース2 \(12\.2\) \- データ型](https://docs.oracle.com/cd/E82638_01/sqlrf/Data-Types.html#GUID-A3C0D836-BADB-44E5-A5D4-265BA5968483)
- [Oracle Database SQL言語リファレンス, 12cリリース2 \(12\.2\) \- 日時書式モデル](https://docs.oracle.com/cd/E82638_01/sqlrf/Format-Models.html#GUID-49B32A81-0904-433E-B7FE-51606672183A)
- [Oracle Database JDBC開発者ガイド, 12cリリース2 \(12\.2\) \- Oracleデータへのアクセスと操作 \- SQL型とJava型間のデフォルト・マッピング](https://docs.oracle.com/cd/E82638_01/jjdbc/accessing-and-manipulating-Oracle-data.html#GUID-1AF80C90-DFE6-4A3E-A407-52E805726778)
- [Oracle JDBC Frequently Asked Questions \- What are the Oracle JDBC releases Vs JDK versions?](https://www.oracle.com/database/technologies/faq-jdbc.html)
- [Maven Central Developers Guide](https://www.oracle.com/database/technologies/maven-central-guide.html)
- [Java \+ H2 Database で主要な JDBC 型の値を取得するサンプルコード \- Qiita](https://qiita.com/niwasawa/items/266bb877003e40e08d6b)
- [Oracle Database 12c を macOS 上の Docker で構築する \- Qiita](https://qiita.com/niwasawa/items/0834e77b0690a5c8501d)