MySQL向けに書かれたアプリケーションをOracle Databaseに接続させる

先日Oracle Database 12c Release 1がリリースされまして、新機能ガイドを眺めていたところ気になる新機能を見つけました。

Oracle Database 12c Release 1にはliboramysql12という共有ライブラリが同梱されていて、これをlibmysqlclientと差し替えるとMySQL向けに書かれたアプリケーションがそのままOracle Databaseに対して動作するのだそうです。ドキュメントから図を引用します。

試してみましょう。MySQLにテスト用のテーブルを作ります。

mysql> CREATE TABLE myora (
    ->     id INT PRIMARY KEY,
    ->     data VARCHAR(100)
    -> ) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4;
Query OK, 0 rows affected (0.08 sec)

mysql> INSERT INTO myora (id, data) VALUES (1, 'マイエスキューエル');
Query OK, 1 row affected (0.01 sec)

mysql> SELECT * FROM myora;
+----+-----------------------------+
| id | data                        |
+----+-----------------------------+
|  1 | マイエスキューエル          |
+----+-----------------------------+
1 row in set (0.00 sec)

Oracle Databaseにも同じものを作ります。

SQL> CREATE TABLE myora (
  2      id NUMBER PRIMARY KEY,
  3      data VARCHAR2(400)
  4  );
Table created.

SQL> INSERT INTO myora (id, data) VALUES (1, 'オラクル');
1 row created.

SQL> COMMIT;
Commit complete.

SQL> COL data FORMAT a40
SQL> SELECT * FROM myora

        ID DATA
---------- ----------------------------------------
         1 オラクル

しばらく試行錯誤していたのですが、PythonMySQL-Pythonの組み合わせで動作確認がとれました。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function
from __future__ import unicode_literals

import MySQLdb

connection = None
cursor = None

try:
    connection = MySQLdb.connect(
            db='scott', user='scott', passwd='tiger', charset='utf8')
    cursor = connection.cursor()
    cursor.execute("SELECT data FROM myora WHERE id = 1")
    result = cursor.fetchall()

    for record in result:
        print(record[0])
except MySQLdb.Error as e:
    print(repr(e))
finally:
    if cursor:
        cursor.close()
    if connection:
        connection.close()
$ python db1.py
マイエスキューエル

$ export LD_LIBRARY_PATH=$ORACLE_HOME/lib
$ export LD_PRELOAD=$ORACLE_HOME/lib/liboramysql12.so
$ python db1.py
オラクル

すごいですね。ただし、LD_PRELOADを設定して実行しただけでは以下のエラーが発生していました。

Traceback (most recent call last):
  File "db1.py", line 16, in <module>
    cursor.execute("SELECT data FROM myora WHERE id = 1")
  File "/usr/lib64/python2.6/site-packages/MySQLdb/cursors.py", line 156, in execute
    query = query.encode(charset)
LookupError: unknown encoding:

これはMySQL C APIのうちmysql_character_set_name()がliboramysql12には実装されておらず、空文字列を返すことによるものです。今回はMySQL-Pythonに以下のパッチをあててしのぎました。

--- cursors.py_orig     2013-07-01 00:57:30.422669501 +0900
+++ cursors.py  2013-06-30 23:01:36.461905484 +0900
@@ -152,6 +152,8 @@
         del self.messages[:]
         db = self._get_db()
         charset = db.character_set_name()
+        if charset == '':
+            charset = 'utf8'
         if isinstance(query, unicode):
             query = query.encode(charset)
         if args is not None:

それから例外ハンドリングの違いが気になりました。以下のスクリプトでわざと一意制約違反を起こしてみます。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function
from __future__ import unicode_literals

import MySQLdb

connection = None
cursor = None

try:
    connection = MySQLdb.connect(
            db='scott', user='scott', passwd='tiger', charset='utf8')
    cursor = connection.cursor()
    cursor.execute("INSERT INTO myora (id, data) VALUES (1, 'ポストグレス')")
    connection.commit()
except MySQLdb.Error as e:
    print(repr(e))
finally:
    if cursor:
        cursor.close()
    if connection:
        connection.close()
$ python db2.py
IntegrityError(1062, "Duplicate entry '1' for key 'PRIMARY'")

$ export LD_LIBRARY_PATH=$ORACLE_HOME/lib
$ export LD_PRELOAD=$ORACLE_HOME/lib/liboramysql12.so
$ python db2.py
InternalError(1, 'ORA-00001: unique constraint (SCOTT.SYS_C0010064) violated')

このように、残念ながらエラー番号のマッピングはしてくれないようです。MySQL-Pythonはエラー番号を見て例外クラスのインスタンスを生成しているため、生成されたインスタンスも誤ったものとなっています。

  • 未実装APIやエラー番号の違いがあるため、MySQL-Pythonなどのドライバにかなりの修正が必要
  • SQLの方言を吸収しないため、アプリケーションにもかなりの修正が必要
  • MySQL用のアプリケーションをわざわざ高価なOracle Databaseに移行する動機がない

ということで現時点ではあまり実用性がない気もしますが、Oracle Database本体がMySQLに関連する機能を備えたのは初めてだと思いますので、ご紹介しました。