[cx_Oracle入門](第16回) LOB型の取り扱い

# 連載目次
[連載：cx_Oracle入門　目次](https://qiita.com/nakaie/items/df170c3fa6895cf1b44d)

# 検証環境
- Oracle Cloud利用
- Oracle Linux 7.7 (VM.Standard2.1)
- Python 3.6
- cx_Oracle 8.0
- Oracle Database 19.6 (DBCS HP, 2oCPU)
- Oracle Instant Client 18.5

# 事前準備
以下のテーブルを作成しておいてください。

```
SQL> create table sample16(col1 number, col2 blob);
```

# LOB型への参照・更新の基本
データが1GBまでであれば、CLOB型であればstr、BLOBであればbyteとして、VARCHAR2型やRAW型のように特段の考慮なく扱えます。1GBを超える場合は一定量ずつ処理するストリーム処理の形式で処理する必要があります。ストリームを介さない方がパフォーマンスには優れますが、1件は1GBに満たないデータであっても、fetchmany()とかするとあっという間にメモリを食いつぶしますので、メモリリソースとフェッチするデータ量(平均サイズ×レコード件数)との兼ね合い次第では、小規模サイズのLOBであってもストリーム処理を検討して下さい。

# 小規模なデータの更新
全ケースにて、処理対象のデータ(ファイル)は別途ご用意ください。サンプルソースを自環境で動かしてみる場合は、ソース内のファイル名もしくは用意したファイルの名称のどちらかを修正してください。以下、BLOB型へのINSERTのサンプルです。

```python:sample16a.py
import cx_Oracle

USERID = "admin"
PASSWORD = "FooBar"
DESTINATION = "atp1_low"
SQL = """
    insert into sample16 values(1, :blobdata)
"""

with open('screenshot1.png', 'rb') as f:
    image = f.read()

with cx_Oracle.connect(USERID, PASSWORD, DESTINATION) as connection:
    with connection.cursor() as cursor:
        cursor.execute(SQL, blobdata=image)
        cursor.execute("commit")
```

# 小規模なデータの参照
参照は更新ほど簡単ではなく、[第9回](https://qiita.com/nakaie/items/1a067c97dade7a7b4efb)に解説したoutputtypehandlerを定義する必要があります。以下のサンプルは、先に登録したレコードを参照して、異なる名称のファイルに保存しています。

```python:sample16b.py
import cx_Oracle

def OutputTypeHandler(cursor, name, defaultType, size, precision, scale):
    if defaultType == cx_Oracle.DB_TYPE_BLOB:
        return cursor.var(cx_Oracle.DB_TYPE_LONG_RAW, arraysize=cursor.arraysize)

USERID = "admin"
PASSWORD = "FooBar"
DESTINATION = "atp1_low"
SQL = """
    select col2 from sample16 where col1 = 1
"""

with cx_Oracle.connect(USERID, PASSWORD, DESTINATION) as connection:
    connection.outputtypehandler = OutputTypeHandler
    with connection.cursor() as cursor:
        cursor.execute(SQL)
        lobdata, = cursor.fetchone()
with open('screenshot2.png', 'wb') as f:
    f.write(lobdata)
```

なお、当然ながら、両ファイルの内容は同じです。

```
$ cmp screenshot1.png screenshot2.png
$
```

# 大規模なデータの更新
BLOBをベースに解説します。おおよそ以下の手順になります。

1. LOB列にSQL関数のEMPTY_BLOB()で空のロケータを挿入
2. INSERT文のRETURNING句でLOBロケータをバインド
3. LOBロケータに対してストリームでデータを流し込む

```python:sample16c.py
import cx_Oracle

USERID = "admin"
PASSWORD = "FooBar"
DESTINATION = "atp1_low"
SQL = """
    insert into sample16 values(2, empty_blob())
    returning col2 into :blobdata
""" # [1.]


with cx_Oracle.connect(USERID, PASSWORD, DESTINATION) as connection:
    with connection.cursor() as cursor:
        blobdata = cursor.var(cx_Oracle.DB_TYPE_BLOB) # [2.]
        cursor.execute(SQL, [blobdata])
        blob, = blobdata.getvalue()
        offset = 1
        bufsize = 65536 # [3.]
        with open('screenshot1.png', 'rb') as f: # [4.]
            while True:
                data = f.read(bufsize)
                if data:
                    blob.write(data, offset)
                if len(data) < bufsize:
                    break
                offset += bufsize
    connection.commit()
```
以下、コメントの番号に対応しています。

1. BLOB列にはSQL関数のEMPTY_BLOB()を使用して空のLOBロケータを挿入しています。そして、RETURNING句を使用して、挿入した空のLOBロケータをバインド変数で受けます。
2. バインド変数に対応するPython側の変数を用意します。Cursor.var()を使用してBLOBであることを指定しています。
3. バッファのサイズを指定しています。
4. オープンしたファイルを3.のサイズ分ずつLOBロケータを介してLOB列に書き込んでいます。

UPDATE文でも、SQL文が変わるだけで流れは同じです。LOB列をEMPTY_BLOB()で更新してください。

# 大規模なデータの参照
BLOBをベースに解説します。おおよそ以下の手順になります。更新よりは簡単です。

1. SELECTリストのLOB列(LOBロケータ)をバインド
2. LOBロケータに対してストリームでデータを読み込む

```python:sample16d.py
import cx_Oracle

USERID = "admin"
PASSWORD = "FooBar"
DESTINATION = "atp1_low"
SQL = """
    select col2 from sample16 where col1 = 2
""" # [1.]


with cx_Oracle.connect(USERID, PASSWORD, DESTINATION) as connection:
    with connection.cursor() as cursor:
        cursor.execute(SQL)
        blob, = cursor.fetchone()
        offset = 1
        bufsize = 65536 # [2.]
        with open('screenshot3.png', 'wb') as f: # [3.]
            while True:
                data = blob.read(offset, bufsize)
                if data:
                    f.write(data)
                if len(data) < bufsize:
                    break
                offset += bufsize
```
以下、コメントの番号に対応しています。

1. BLOB列をSELECTします。
2. バッファのサイズを指定しています。
3. オープンしたファイルに、LOBのデータを2.のサイズ分ずつ書き込んでいます。