[cx_Oracle入門](第6回) DBとPythonのデータ型のマッピング(バージョン7.3まで)

# 検証環境
- Oracle Cloud利用
- Oracle Linux 7.7 (VM.Standard2.1)
- Python 3.6
- cx_Oracle 7.3
- Oracle Database 19.5 (ATP, 1OCPU)
- Oracle Instant Client 18.5

# はじめに
Oracle DatabaseとPythonでは当然ながらデータ型が異なります。本記事では、cx_Oracleがどのように両者のデータ型の仲立ちを行うのかを解説します。

# データ型のマッピングの概要
Oracle Databaseのどのデータ型が、最終的にどのPythonのデータ型にマッピングされるのかは、cx_Oracleのマニュアルにまとめられています。Oracle Databaseから取得されたデータは、一旦cx_Oracleのデータ型のデータに変換され、cx_Oracleのデータ型を経由してPythonのデータ型に格納されます。更新データに関しては逆の流れとなります。以下、主なデータ型の対応と、マニュアルの参照先です。

■ [DB API](https://www.python.org/dev/peps/pep-0249/)に準拠しているデータ型のマッピング

| Oracle Databaseのデータ型 |cx_Oracleのデータ型 | Pythonのデータ型
|:-----------------|:------------------|:------------------|
| CHAR, VARCHAR2 | cx_Oracle.STRING | str |
| NUMBER | cx_Oracle.NUMBER | float もしくは int |
| DATE | cx_Oracle.DATETIME | datetime.datetime |
| TIMESTAMP | cx_Oracle.TIMESTAMP | datetime.datetime |
| RAW | cx_Oracle.BINARY | bytes |

■マニュアル参照先
[DB APIに準拠しているcx_Oracleデータ型](https://cx-oracle.readthedocs.io/en/latest/api_manual/module.html#db-api-types)
[cx_Oracle独自のデータ型](https://cx-oracle.readthedocs.io/en/latest/api_manual/module.html#database-type-synonyms)
※バージョン8の登場に伴い、7.3までのcx_Oracle独自のデータ型は将来廃止予定のシノニム扱いとなっています。当然ながらバージョンン8で改められた型は7.3では使用できないので、シノニムをご利用ください。

ここで注意しないといけないのは、NUMBER型に対応したPythonのデータ型に、floatとintの2種類が存在する点です。これは、NUMBER型の定義や格納されている値に依存します。以下のサンプルアプリケーションの実行結果を確認してください。

```python:sample06a.py
import cx_Oracle

USERID = "admin"
PASSWORD = "FooBar"
DESTINATION = "atp1_low"
SQL1 = """
        create table sample06a (col1 number, col2 number, col3 number,
          col4 number(5, 0), col5 number(5, 0), col6 number(5, 2),
          col7 number(5, 2), col8 number(5, 2))
"""
SQL2 = "insert into sample06a values(7, 7.0, 7.1, 7, 7.0, 7, 7.0, 7.1)"
SQL3 = "commit"
SQL4 = "select * from sample06a"

with cx_Oracle.connect(USERID, PASSWORD, DESTINATION) as connection:
        with connection.cursor() as cursor:
                cursor.execute(SQL1)
                cursor.execute(SQL2)
                cursor.execute(SQL3)
                row = cursor.execute(SQL4).fetchone()
                print(f"NUMBERに「7」 : {type(row[0])}")
                print(f"NUMBERに「7.0」 : {type(row[1])}")
                print(f"NUMBERに「7.1」 : {type(row[2])}")
                print(f"NUMBER(5, 0)に「7」 : {type(row[3])}")
                print(f"NUMBER(5, 0)に「7.0」 : {type(row[4])}")
                print(f"NUMBER(5, 2)に「7」 : {type(row[5])}")
                print(f"NUMBER(5, 2)に「7.0」 : {type(row[6])}")
                print(f"NUMBER(5, 2)に「7.1」 : {type(row[7])}")
```

```:実行結果
$ python sample06a.py
NUMBERに「7」 : <class 'int'>
NUMBERに「7.0」 : <class 'int'>
NUMBERに「7.1」 : <class 'float'>
NUMBER(5, 0)に「7」 : <class 'int'>
NUMBER(5, 0)に「7.0」 : <class 'int'>
NUMBER(5, 2)に「7」 : <class 'float'>
NUMBER(5, 2)に「7.0」 : <class 'float'>
NUMBER(5, 2)に「7.1」 : <class 'float'>
```

実行結果から、以下の法則が見受けられます。

- 精度なしのNUMBER型の場合、0以外の小数がある場合はfloat型、そうではない場合はint型
- 小数部が0のNUMBER型の場合、常にint型
- 小数部が0ではないNUMBER型の場合、常にfloat型

int型の場合は特段問題ありませんが、問題はfloat型です。Oracle Databaseがよく利用されるビジネスアプリケーションの場合、特にお金に関する情報で浮動小数だと丸め誤差が発生して問題となる可能性が懸念されます。そのような場合、Pythonではdecimalモジュールを使用して処理しますが、前述の表のとおり、cx_Oracle自身はdecimalへの変換を行いません。ただし、cx_Oracleはこのような場合の備えを用意しています。

# outputtypehandler
先述のような理由で、cx_Oracleのデフォルトのデータ型の変換仕様を利用したくない場合、Connectionオブジェクトのoutputtypehandler属性に自作のデータ変換関数を指定すると、オリジナルの変換ルールではなく、その関数を変換に使用するようになります。Python→Oracleの方向の場合はinputtypehandler属性となります。

```python:sample06b.py
import cx_Oracle
import decimal

USERID = "admin"
PASSWORD = "FooBar"
DESTINATION = "atp1_low"
SQL = "select * from sample06a"

def num2Dec(cursor, name, defaultType, size, precision, scale):
        if defaultType == cx_Oracle.NUMBER:
                return cursor.var(decimal.Decimal, arraysize=cursor.arraysize)

with cx_Oracle.connect(USERID, PASSWORD, DESTINATION) as connection:
        with connection.cursor() as cursor:
                row = cursor.execute(SQL).fetchone()
                print(f"outputtypehandlerなし")
                print(f"NUMBERに「7」をセットして3倍 : {row[0] * 3}")
                print(f"NUMBERに「7.1」をセットして3倍 : {row[2] * 3}")
        with connection.cursor() as cursor:
                cursor.outputtypehandler = num2Dec
                row = cursor.execute(SQL).fetchone()
                print(f"outputtypehandlerあり")
                print(f"NUMBERに「7」をセットして3倍 : {row[0] * 3}")
                print(f"NUMBERに「7.1」をセットして3倍 : {row[2] * 3}")
```

このスクリプトは先のスクリプトで作成したテーブルとデータをSELECTしている点、ご注意ください。
スクリプトの真ん中あたりのnum2Dec関数が実際の新しいデータ変換ルーチンとなります。下から5行目にて、outputtypehandlerとしてnum2Dec関数をセットすることで、この関数が機能するようになります。
outputtypehandlerの関数名や引数名は任意のものを指定できますが、引数の仕様は以下のとおりに定められており、関数内で使用しなくとも、6つとも引数として必要です。

| 引数の順番 | 意味
|:-----------------|:------------------|
| 1 | 操作対象のCursorオブジェクト |
| 2 | 列名 |
| 3 | 列のcx_Oracleのデータ型 |
| 4 | 列のサイズ |
| 5 | 列の小数桁数(NUMBER(p,s)のs) |
| 6 | 列の全体桁数(NUMBER(p,s)のp) |

サンプル中のCursorオブジェクトのvarメソッドは、該当する列の変数に関して、変数の情報を引数で指定した形に更新するメソッドとなります。1個目の引数には変更先のデータ型を指定します。指定必須です。varメソッド自体はoutputtypehandler以外の用途にも広く使われるメソッドで、メソッドの仕様としては2個目以降の引数は任意指定ですが、outputtypehandler用途の場合、arraysizeというパラメータが必須となり、Cursorオブジェクトのarraysizeを設定する必要があります。

```:実行結果
$ python sample06b.py
outputtypehandlerなし
NUMBERに「7」をセットして3倍 : 21
NUMBERに「7.1」をセットして3倍 : 21.299999999999997
outputtypehandlerあり
NUMBERに「7」をセットして3倍 : 21
NUMBERに「7.1」をセットして3倍 : 21.3
```

実行結果のように、outputtypehandlerを経由させると、より期待する計算結果になっています。outputtypehandlerを使用せず、一旦Pythonのfloat型の変数で受けてからdecimalに変換する形でももちろん構いませんが、対応列数が多い場合などはoutputtypehandlerを使用すると楽にコーディングできます。

# 変更履歴
- 2020/6/28
 - バージョン8での仕様変更に伴う記述の見直し
 - バージョン8でマニュアルが変更されたことに伴う、マニュアルリンク先の変更