Java 8 用 O/R マッピング・ライブラリ Lightsleep の柔軟なデータ型変換の仕組み

Lightsleep に関しては、紹介記事を Qiita に投稿していますので、下記をご覧ください。

<a href="http://qiita.com/MasatoKokubo/items/ab46696b203d7f67036c" target="other">
    Java Runtime と JDBC ドライバーだけで動作する O/R マッピング・ライブラリ Lightsleep の紹介
</a>

Lightsleep には、柔軟なデータ型変換を行う仕組みがあり、それを内部で使用しています。Lightsleep を使用する上で、その詳細を知る事は必須ではありませんが、どのような変換が行われるかを知っておくと役に立つと思います。

### データ型変換の使用箇所

主に以下の2箇所で使用します。

1. SQL を生成するために、エンティティ・オブジェクトのプロパティ値をリテラル文字列に変換する所  
  例えば、
    - `"Yukari's apples"` ➔ `'Yukari''s apples'`
    - `2017/2/12` (java.sql.Date) ➔ `DATE'2017-02-17'`

1. DBから取得した値をエンティティ・オブジェクトに設定する所  
  例えば、
    - BigDecimal ➔ Integer (カラム型が NUMBER(9) / Oracle)
    - Long ➔ java.sql.Date (カラム型が BIGINT)

上記の 2 が本来のデータ型変換の目的で、格納する値とそれを格納する変数の型が異なる場合でも格納できるようにします。

1 は、この仕組みを利用して SQL のリテラル文字列の生成を行っています。通常の文字列と変換内容が異なるため、変換先データ型は SqlString クラスとしています。データベース・ハンドラ毎にデータ型変換マップを持つ事で、DBMS 固有の SQL の生成を可能にしています。

### データ型変換を行うクラス

org.lightsleep.helper.TypeConverter がデータ型変換を行うクラスです。Lightsleep を利用する場合に、このクラスを直接利用する事は、あまりないと思いますが簡単に説明します。

このクラスには以下の2つのコンストラクタがあります。

1. `TypeConverter(Class<ST> sourceType, Class<DT> destinType, Function<ST, DT> function)`
2. `<MT> TypeConverter(TypeConverter<ST, MT> typeConverter1, TypeConverter<MT, DT> typeConverter2)`

コンストラクタ1の引数は、変換元のデータ型(sourceType)、変換先のデータ型(destinType)、変換を行う Function オブジェクト(function)です。function は、ラムダ式で記述します。

```java:Long->java.sql.Date
    new TypeConverter<>(Long.class, Date.class, object -> new Date(object))
```

コンストラクタ2の引数は、他の TypeConverter オブジェクト2つです。

```java:java.util.Date->Integer
    new TypeConverter<>(
        TypeConverter.get(typeConverterMap, java.util.Date.class, Long.class),
        TypeConverter.get(typeConverterMap, Long.class, Integer.class)
    )
```

上記例での `static TypeConverter.get` メソッド は、マップに登録してある TypeConverter オブジェクトを取得するメソッドです。この TypeConverter オブジェクトの Function オブジェクトは、typeConverter1.function.andThen(typeConverter2.function)` (合成関数) になります。

TypeConverter オブジェクトは、変換元データ型と変換先データ型から生成したキーを持っているため、マップ登録の際は、`Map#put` ではなく、`static TypeConverter#put` メソッドを使用します。

```java:Long->java.sql.Date
TypeConverter.put(typeConverterMap,
    new TypeConverter<>(Long.class, Date.class, object -> new Date(object))
);
```

```java:java.util.Date->Integer
TypeConverter.put(typeConverterMap,
    new TypeConverter<>(
        TypeConverter.get(typeConverterMap, java.util.Date.class, Long.class),
        TypeConverter.get(typeConverterMap, Long.class, Integer.class)
    )
);
```

データ型の変換処理は、`static TypeConverter#convert` メソッドで行います。このメソッドは以下の手順で変換処理を行います。

1. 引数の変換元オブジェクトの型と変換先データ型からマップのキーを生成。
1. 引数のデータ変換型用マップから TypeConverter オブジェクトを取得。
1. TypeConverter オブジェクトが持つ Function オブジェクトの apply メソッドをコールしてデータ型を変換。

ただし引数の変換元オブジェクトが null または変換先データ型にキャスト可能である場合は、変換処理を行わずにそのまま変換元オブジェクトを返します。

2 では、`static TypeConverter#get` が使用されますが、指定の変換元データ型と変換先データ型の組み合わせで見つからない場合は、変換元データ型をその型のインタフェースやスーパークラスで見つからないを試します。
見つかった場合は、その変換用関数を使用できるので、次回の変換処理で単純に TypeConverter オブジェクト検索できるように見つかったデータ型と変換先データ型からキーを作成してマップに登録します。
最終的に変換可能な TypeConverter オブジェクト見つからない場合は、ConvertException をスローします。

### TypeConverter オブジェクトの格納場所

TypeConverter オブジェクトは、以下のクラスの `Map<String, TypeConverter<?, ?>>` 型の変数に格納されています。

1. TypeConverter クラス
1. Standard クラスおよびそのサブクラス (MySQL, Oracle, PostgreSQL, SQLite, SQLServer)

1 は静的変数のマップを持ち、2 はインスタンス変数のマップを持っています。  
Standard クラスおよびそのサブクラスは、シングルトン・クラスで、クラス毎に単一のオブジェクトしか存在しないため、1 と同様にクラス毎に1つのデータ型変換マップが存在する事になります。

データ型変換処理で通常使用されるのは、2 の方で、そのうちのどれが使用されるかは、lightsleep.propeties の Database プロパティの指定で決まります。

### 各クラスのデータ型変換マップに格納される TypeConverter オブジェクト

#### TypeConverter クラス

このクラスのデータ型変換マップには、O/R マッピングとは無関係な、一般的なデータ型変換を行う以下の TypeConverter オブジェクトが格納されています。

<table>
  <tr><th>変換元データ型</th><th>変換先データ型</th><th>変換内容</th></tr>

  <tr><td>Byte          </td><td rowspan="9">Boolean</td><td rowspan="7">0 ➔ false<br>1 ➔ true<br>その他の場合 ConvertException をスロー</td></tr>
  <tr><td>Short         </td></tr>
  <tr><td>Integer       </td></tr>
  <tr><td>Long          </td></tr>
  <tr><td>Float         </td></tr>
  <tr><td>Double        </td></tr>
  <tr><td>BigDecimal    </td></tr>
  <tr><td>Character     </td><td>'0' ➔ false<br>'1' ➔ true<br>その他の場合 ConvertException をスロー</td></tr>
  <tr><td>String        </td><td>"0" ➔ false<br>"1" ➔ true<br>その他の場合 ConvertException をスロー</td></tr>

  <tr><td>Boolean       </td><td rowspan="9">Byte</td><td>false ➔ 0<br>true ➔ 1</td></tr>
  <tr><td>Short         </td><td rowspan="7">範囲外の場合 ConvertException をスロー</td></tr>
  <tr><td>Integer       </td></tr>
  <tr><td>Long          </td></tr>
  <tr><td>Float         </td></tr>
  <tr><td>Double        </td></tr>
  <tr><td>BigDecimal    </td></tr>
  <tr><td>Character     </td></tr>
  <tr><td>String        </td><td>非数値または範囲外の場合 ConvertException をスロー</td></tr>

  <tr><td>Boolean       </td><td rowspan="9">Short</td><td>false ➔ 0<br>true ➔ 1</td></tr>
  <tr><td>Byte          </td><td></td></tr>
  <tr><td>Integer       </td><td rowspan="6">範囲外の場合 ConvertException をスロー</td></tr>
  <tr><td>Long          </td></tr>
  <tr><td>Float         </td></tr>
  <tr><td>Double        </td></tr>
  <tr><td>BigDecimal    </td></tr>
  <tr><td>Character     </td></tr>
  <tr><td>String        </td><td>非数値または範囲外の場合 ConvertException をスロー</td></tr>

  <tr><td>Boolean       </td><td rowspan="10">Integer</td><td>false ➔ 0<br>true ➔ 1</td></tr>
  <tr><td>Byte          </td><td></td></tr>
  <tr><td>Short         </td><td></td></tr>
  <tr><td>Long          </td><td rowspan="4">範囲外の場合 ConvertException をスロー</td></tr>
  <tr><td>Float         </td></tr>
  <tr><td>Double        </td></tr>
  <tr><td>BigDecimal    </td></tr>
  <tr><td>Character     </td><td></td></tr>
  <tr><td>String        </td><td>非数値または範囲外の場合 ConvertException をスロー</td></tr>
  <tr><td>java.util.Date</td><td>範囲外の場合 ConvertException をスロー</td></tr>

  <tr><td>Boolean       </td><td rowspan="10">Long</td><td>false ➔ 0<br>true ➔ 1</td></tr>
  <tr><td>Byte          </td><td></td></tr>
  <tr><td>Short         </td><td></td></tr>
  <tr><td>Integer       </td><td></td></tr>
  <tr><td>Float         </td><td rowspan="3">範囲外の場合 ConvertException をスロー</td></tr>
  <tr><td>Double        </td></tr>
  <tr><td>BigDecimal    </td></tr>
  <tr><td>Character     </td><td></td></tr>
  <tr><td>String        </td><td>非数値または範囲外の場合 ConvertException をスロー</td></tr>
  <tr><td>java.util.Date</td><td>long 値を取得</td></tr>

  <tr><td>Boolean       </td><td rowspan="9">Float</td><td>false ➔ 0.0F<br>true ➔ 1.0F</td></tr>
  <tr><td>Byte          </td><td></td></tr>
  <tr><td>Short         </td><td></td></tr>
  <tr><td>Integer       </td><td></td></tr>
  <tr><td>Long          </td><td></td></tr>
  <tr><td>Double        </td><td></td></tr>
  <tr><td>BigDecimal    </td><td></td></tr>
  <tr><td>Character     </td><td></td></tr>
  <tr><td>String        </td><td>非数値の場合 ConvertException をスロー</td></tr>

  <tr><td>Boolean       </td><td rowspan="9">Double</td><td>false ➔ 0.0D<br>true ➔ 1.0D</td></tr>
  <tr><td>Byte          </td><td></td></tr>
  <tr><td>Short         </td><td></td></tr>
  <tr><td>Integer       </td><td></td></tr>
  <tr><td>Long          </td><td></td></tr>
  <tr><td>Float         </td><td></td></tr>
  <tr><td>BigDecimal    </td><td></td></tr>
  <tr><td>Character     </td><td></td></tr>
  <tr><td>String        </td><td>非数値の場合 ConvertException をスロー</td></tr>

  <tr><td>Boolean       </td><td rowspan="9">BigDecimal</td><td>false ➔ <code>BigDecimal.ZERO</code><br>true ➔ <code>BigDecimal.ONE</code></td></tr>
  <tr><td>Byte          </td><td></td></tr>
  <tr><td>Short         </td><td></td></tr>
  <tr><td>Integer       </td><td></td></tr>
  <tr><td>Long          </td><td></td></tr>
  <tr><td>Float         </td><td></td></tr>
  <tr><td>Double        </td><td></td></tr>
  <tr><td>Character     </td><td></td></tr>
  <tr><td>String        </td><td>非数値の場合 ConvertException をスロー</td></tr>

  <tr><td>Boolean       </td><td rowspan="9">Character</td><td>false ➔ '0'<br>true ➔ '1'</td></tr>
  <tr><td>Byte          </td><td></td></tr>
  <tr><td>Short         </td><td></td></tr>
  <tr><td>Integer       </td><td rowspan="5">範囲外の場合 ConvertException をスロー</td></tr>
  <tr><td>Long          </td></tr>
  <tr><td>Float         </td></tr>
  <tr><td>Double        </td></tr>
  <tr><td>BigDecimal    </td></tr>
  <tr><td>String        </td><td>String の長さが1以外の場合 ConvertException をスロー</td></tr>

  <tr><td>BigDecimal    </td><td rowspan="6">String</td><td>toPlainString() で変換</td></tr>
  <tr><td>java.uitl.Date</td><td rowspan="2"><code>"yyyy-MM-dd"</code></td></tr>
  <tr><td>java.sql.Date </td></tr>
  <tr><td>Time          </td><td><code>"HH:mm:ss"</code></td></tr>
  <tr><td>Timestamp     </td><td><code>"yyyy-MM-dd HH:mm:ss.SSS"</code></td></tr>
  <tr><td>Object        </td><td>toString() で変換</td></tr>

  <tr><td>Integer       </td><td rowspan="4">java.util.Date</td><td></td></tr>
  <tr><td>Long          </td><td></td></tr>
  <tr><td>BigDecimal    </td><td>Long への変換で範囲外の場合 ConvertException をスロー</td></tr>
  <tr><td>String        </td><td><code>"yyyy-MM-dd"</code> ➔ String<br>フォーマット不正の場合 ConvertException をスロー</td></tr>

  <tr><td>Integer       </td><td rowspan="5">java.sql.Date</td><td></td></tr>
  <tr><td>Long          </td><td></td></tr>
  <tr><td>BigDecimal    </td><td>Long への変換で範囲外の場合 ConvertException をスロー</td></tr>
  <tr><td>java.util.Date</td><td></td></tr>
  <tr><td>String        </td><td><code>"yyyy-MM-dd"</code> ➔ String<br>フォーマット不正の場合 ConvertException をスロー</td></tr>

  <tr><td>Integer       </td><td rowspan="5">Time</td><td></td></tr>
  <tr><td>Long          </td><td></td></tr>
  <tr><td>BigDecimal    </td><td>Long への変換で範囲外の場合 ConvertException をスロー</td></tr>
  <tr><td>java.util.Date</td><td></td></tr>
  <tr><td>String        </td><td><code>"HH:mm:ss"</code> ➔ String<br>フォーマット不正の場合 ConvertException をスロー</td></tr>

  <tr><td>Long          </td><td rowspan="5">Timestamp</td><td></td></tr>
  <tr><td>Integer       </td><td></td></tr>
  <tr><td>BigDecimal    </td><td>Long への変換で範囲外の場合 ConvertException をスロー</td></tr>
  <tr><td>java.util.Date</td><td></td></tr>
  <tr><td>String        </td><td><code>"yyyy-MM-dd HH:mm:ss"</code> または<br><code>"yyyy-MM-dd HH:mm:ss.SSS"</code> ➔ String<br>フォーマット不正の場合 ConvertException をスロー</td></tr>

  <tr><td rowspan="4">Enum</td><td>Byte   </td><td rowspan="2">ordinal() で変換<br>範囲外の場合 ConvertException をスロー</td></tr>
  <tr>                         <td>Short  </td></tr>
  <tr>                         <td>Integer</td><td rowspan="2">ordinal() で変換</td></tr>
  <tr>                         <td>Long   </td></tr>
</table>


#### Standard クラス

このクラスのデータ型変換マップには、TypeConverter クラスが持つすべての TypeConverter オブジェクトに、以下の O/R マッピングに固有の TypeConverter オブジェクトが追加されます。

<table>
  <tr><th>変換元データ型</th><th>変換先データ型</th><th>変換内容</th></tr>

  <tr><td>Clob          </td><td>String</td><td rowspan="2">長さが <code>Integer.MAX_VALUE</code> を超える場合 ConvertException をスロー<br>内容の取得時に SQLException がスローされた場合 ConvertException をスロー</td></tr>

  <tr><td>Blob          </td><td>byte[]</td></tr>

  <tr><td rowspan="13">java.sql.Array</td><td>boolean[]       </td><td rowspan="13">各要素を TypeConverter で配列要素のデータ型に変換</td></tr>
  <tr>                                    <td>byte[]          </td></tr>
  <tr>                                    <td>short[]         </td></tr>
  <tr>                                    <td>int[]           </td></tr>
  <tr>                                    <td>long[]          </td></tr>
  <tr>                                    <td>float[]         </td></tr>
  <tr>                                    <td>double[]        </td></tr>
  <tr>                                    <td>BigDecimal[]    </td></tr>
  <tr>                                    <td>String[]        </td></tr>
  <tr>                                    <td>java.util.Date[]</td></tr>
  <tr>                                    <td>java.sql.Date[] </td></tr>
  <tr>                                    <td>Time[]          </td></tr>
  <tr>                                    <td>Timestamp[]     </td></tr>

  <tr><td>Boolean        </td><td rowspan="26">SqlString</td><td>false ➔ <code>FALSE</code><br>true ➔ <code>TRUE</code></td></tr>
  <tr><td>Object         </td><td rowspan="2"><code>'...'</code></td></tr>
  <tr><td>Character      </td></tr>
  <tr><td>BigDecimal     </td><td></td></tr>
  <tr><td>String         </td><td><code>'...'</code><br>長い場合は <code>?</code> <i>(SQLパラメータ)</i></td></tr>
  <tr><td>java.util.Date</td><td rowspan="2"><code>DATE'yyyy-MM-dd'</code></td></tr>
  <tr><td>java.sql.Date  </td></tr>
  <tr><td>Time           </td><td><code>TIME'HH:mm:ss'</code></td></tr>
  <tr><td>Timestamp      </td><td><code>TIMESTAMP'yyyy-MM-dd HH:mm:ss.SSS'</code></td></tr>
  <tr><td>Enum           </td><td><code>'...'</code> (toString() で変換)</td></tr>
  <tr><td>byte[]         </td><td><code>X'...'</code><br>長い場合は <code>?</code> <i>(SQLパラメータ)</i></td></tr>
  <tr><td>boolean[]      </td><td rowspan="14"><code>ARRAY[x,y,z,...]</code><br>各要素を TypeConverter で SqlString に変換</td></tr>
  <tr><td>char[]         </td></tr>
  <tr><td>byte[][]       </td></tr>
  <tr><td>short[]        </td></tr>
  <tr><td>int[]          </td></tr>
  <tr><td>long[]         </td></tr>
  <tr><td>float[]        </td></tr>
  <tr><td>double[]       </td></tr>
  <tr><td>BigDecimal[]   </td></tr>
  <tr><td>String[]       </td></tr>
  <tr><td>java.util.Date[]</td></tr>
  <tr><td>java.sql.Date[]</td></tr>
  <tr><td>Time[]         </td></tr>
  <tr><td>Timestamp[]    </td></tr>
  <tr><td>Iterable       </td><td><code>(x,y,z,...)</code><br>各要素を TypeConverter で SqlString に変換</td></tr>
</table>

#### MySQL クラス

このクラスのデータ型変換マップには、Standard クラスが持つすべての TypeConverter オブジェクトに、MySQL に固有の TypeConverter オブジェクトが追加されます。

<table>
  <tr><th>変換元データ型</th><th>変換先データ型</th><th>変換内容</th></tr>
  <tr><td>Boolean</td><td rowspan="2">SqlString</td><td>false ➔ <code>0</code><br>true ➔ <code>1</code></td></tr>
  <tr><td>String </td><td><code>'...'</code><br>制御文字はエスケープ・シーケンスに変換<br>長い場合は <code>?</code> <i>(SQLパラメータ)</i></td></tr>
</table>

#### Oracle クラス

このクラスのデータ型変換マップには、Standard クラスが持つすべての TypeConverter オブジェクトに、Oracle Database に固有の TypeConverter オブジェクトが追加されます。

<table>
  <tr><th>変換元データ型</th><th>変換先データ型</th><th>変換内容</th></tr>
  <tr><td>Boolean</td><td rowspan="4">SqlString</td><td>false ➔ <code>0</code><br>true ➔ <code>1</code></td></tr>
  <tr><td>String </td><td><code>'...'</code><br>制御文字は <code>'...'||CHR(n)||'...'</code> に変換<br>長い場合は <code>?</code> <i>(SQLパラメータ)</i></td></tr>
  <tr><td>Time   </td><td><code>TO_TIMESTAMP('1970-01-01 HH:mm:ss','YYYY-MM-DD HH24:MI:SS.FF3')</code></td></tr>
  <tr><td>byte[]</td><td><code>?</code> <i>(SQLパラメータ)</i></td></tr>
  <tr><td rowspan="4">oracle.sql.TIMESTAMP</td><td>java.util.Date</td><td rowspan="4">値の取得時に SQLException がスローされた場合 ConvertException をスロー</td></tr>
  <tr>                                         <td>java.sql.Date     </td></tr>
  <tr>                                         <td>java.sql.Time     </td></tr>
  <tr>                                         <td>java.sql.Timestamp</td></tr>
</table>

#### PostgreSQL クラス

このクラスのデータ型変換マップには、Standard クラスが持つすべての TypeConverter オブジェクトに、PostgreSQL に固有の TypeConverter オブジェクトが追加されます。

<table>
  <tr><th>変換元データ型</th><th>変換先データ型</th><th>変換内容</th></tr>
  <tr><td>String </td><td rowspan="2">SqlString</td><td><code>'...'</code><br>制御文字は エスケープ・シーケンスに変換<br>長い場合は <code>?</code> <i>(SQLパラメータ)</i></td></tr>
  <tr><td>byte[] </td><td><code>E'\\x...'</code><br>長い場合は <code>?</code> <i>(SQLパラメータ)</i></td></tr>
</table>

#### SQLite クラス

このクラスのデータ型変換マップには、Standard クラスが持つすべての TypeConverter オブジェクトに、SQLite に固有の TypeConverter オブジェクトが追加されます。

<table>
  <tr><th>変換元データ型</th><th>変換先データ型</th><th>変換内容</th></tr>
  <tr><td>Boolean       </td><td rowspan="6">SqlString</td><td>false ➔ <code>0</code><br>true ➔ <code>1</code></td></tr>
  <tr><td>java.util.Date</td><td rowspan="2"><code>'yyyy-MM-dd'</code></td></tr>
  <tr><td>java.sql.Date </td></tr>
  <tr><td>Time          </td><td><code>'HH:mm:ss'</code></td></tr>
  <tr><td>Timestamp     </td><td><code>'yyyy-MM-dd HH:mm:ss.SSS'</code></td></tr>
  <tr><td>byte[]        </td><td><code>?</code> <i>(SQLパラメータ)</i></td></tr>
</table>

#### SQLServer クラス

このクラスのデータ型変換マップには、Standard クラスが持つすべての TypeConverter オブジェクトに、Microsoft SQL Server に固有の TypeConverter オブジェクトが追加されます。

<table>
  <tr><th>変換元データ型</th><th>変換先データ型</th><th>変換内容</th></tr>
  <tr><td>Boolean      </td><td rowspan="6">SqlString</td><td>false ➔ <code>0</code><br>true ➔ <code>1</code></td></tr>
  <tr><td>java.sql.Date</td><td><code>CAST('yyyy:MM:dd' AS DATE)</code></td></tr>
  <tr><td>Time         </td><td><code>CAST('HH:mm:ss' AS DATE)</code></td></tr>
  <tr><td>Timestamp    </td><td><code>CAST('yyyy-MM-dd HH:mm:ss.SSS' AS DATETIME2)</code></td></tr>
  <tr><td>String       </td><td><code>'...'</code><br>制御文字は <code>'...'+CHAR(n)+'...'</code> に変換<br>長い場合は<code>?</code> <i>(SQLパラメータ)</i></td></tr>
  <tr><td>byte[]</td><td><code>?</code> <i>(SQLパラメータ)</i></td></tr>
</table>