Oracle PL/SQLからMySQLへの移行苦労話

Oracleをバックエンドとする老朽化システムのマイグレーションにおいて、ライセンス料の課題から、無償のMySQLRDBをダウンサイジングする、というのはよくある話です。しかし、テーブルデータだけではなく、サーバサイドに実装されているOracle PL/SQLがあった場合、これらもMySQLに移植し、クライアントの移行コストを抑える、という選択肢が出てきます。

ところが、OraclePL/SQLMySQLのサーバ処理(Procedure、Function)は、仕様的にいくつかの差があるため、実際に移行しようとすると、なかなか骨が折れます。今回は、実際に移行してみた経験から、大変だった苦労話をいくつかピックアップしてご紹介します。

パラメータ付カーソルが使えない

PL/SQLで複数行のデータ処理を行う場合、よくカーソルを使用します。MySQLにもカーソルは存在するのですが、こちらでは引数を指定する(Oracleのパラメータ付カーソル)ことができません。

このため、MySQLでカーソルの抽出条件を動的に変更するためには、予めカーソルが参照数ための広域変数を定義し、カーソルをOPENする前に、毎回変数に値をセットする必要があります。

<Oracleの場合>

  -- カーソル定義
  CURSOR cur(pi_num1 INT, pi_num2 INT) IS -- パラメタ付きでカーソルを定義する
    SELECT fieldA, filedB
      FROM table1
     WHERE field1 = pi_num1
       AND field2 = pi_num2;

BEGIN
  -- 処理部
  OPEN cur(11, 22); CLOSE cur;   -- 引数として条件を指定できる
  OPEN cur(33, 44); CLOSE cur;   
  OPEN cur(55, 66); CLOSE cur;   

<MySQLの場合>

BEGIN
  -- 変数定義
  DECLARE pi_num1    INT;      -- 条件に使用する変数を定義する
  DECLARE pi_num2    INT;

  -- カーソル定義
  DECLARE cur CURSOR FOR 
    SELECT fieldA, filedB
      FROM table1
     WHERE field1 = pi_num1    -- 定義した変数をSQLに渡す
       AND field2 = pi_num2;

  -- 処理部
  SET pi_num1 = 11;            -- カーソルOPEN前に、毎回変数をセットする
  SET pi_num2 = 22;
  OPEN cur; CLOSE cur;

  SET pi_num1 = 33;
  SET pi_num2 = 44;
  OPEN cur; CLOSE cur;

  SET pi_num1 = 55;
  SET pi_num2 = 66;
  OPEN cur; CLOSE cur;

パラメタによって、同一の問合せをカーソルで再利用できるメリットが半減してしまい、コードも冗長となります。

%ROWTYPEが使用できない

Oracleでは変数の一種として、テーブルの行を模した「%ROWTYPE」が使用できます。これは、Procedureの引数の返り値のI/Oや、先のカーソルのFETCH結果の受け取りで使用されます。

しかし、MySQLにはこの「%ROWTYPE」がありません。テーブルの行そのものを変数に格納することができないため、必要となるフィールド毎に個別に変数を定義して、それぞれ代入・取り出し操作を行う必要があります。

<Oracleの場合>

  -- カーソル定義
  CURSOR cur IS
    SELECT *
      FROM table1;

  rec    table1%ROWTYPE;   -- %ROWTYPEで変数recを宣言

BEGIN
  -- 処理部
  OPEN cur;
  FETCH cur INTO rec;                               -- recに、table1の1行文が取り出される
  result := rec.fieldA || rec.fieldB || rec.fieldC; -- rec.[列名]で、任意の値が取得できる
  RETURN result;

<MySQLの場合>

BEGIN

  -- 変数定義
  DECLARE rec_fieldA    VARCHAR(10);   -- フィールド毎の格納変数を宣言
  DECLARE rec_fieldB    VARCHAR(10);
  DECLARE rec_fieldC    VARCHAR(10);

  DECLARE cur CURSOR FOR 
    SELECT fieldA, filedB, fieldC      -- FETCHで受け取るフィールドを個別に指定 ※
      FROM table1;

  OPEN cur; CLOSE cur;
  FETCH cur INTO rec_fieldA, rec_fieldB, rec_fieldC;  -- SELECT句と同じ並びで変数を指定
  SET result = CONCAT(rec.fieldA, rec.fieldB, rec.fieldC);

SQL部分を、Oracleのように SELECT * FROM ~とすることも可能ですが、その場合は、FETCH INTO に対象テーブルのすべてのフィールドを列挙しないといけません。フィールド数不一致でエラーとなります。

変数宣言、SQLのSELECT句、FETCHのINTO先、いずれにも、カーソルから取得するフィールドを列挙する必要があるため、コードが非常に大きくなります。

このROWTYPEと、前述のパラメタ付カーソルが使用できない件によって、PL/SQLのカーソルをMySQLに移植すると、次のように非常に長いコードになります。

<MySQLのカーソル使用例>

BEGIN
  -- 変数宣言
  DECLARE ci_fieldA  AS INT;
  DECLARE ci_fieldB  AS INT;
  DECLARE ci_fieldC  AS INT;
  DECLARE ci_fieldD  AS INT;
  DECLARE ci_fieldE  AS INT;
  -- カーソルにINPUTしたい変数をすべて定義

  DECLARE co_field1  AS INT;
  DECLARE co_field2  AS INT;
  DECLARE co_field3  AS INT;
  DECLARE co_field4  AS INT;
  DECLARE co_field5  AS INT;
  -- カーソルからOUTPUTしたいフィールド分すべて定義

   -- カーソル宣言
  DECLARE cur CURSOR FOR
  SELECT fiedl1, field2, field3, field4, field5 ...  -- 取得したいフィールドはすべて列挙
    FROM table1
   WHERE fieldA = ci_fieldA  
     AND fieldB = ci_fieldB  
     AND fieldC = ci_fieldC;  
     AND fieldD = ci_fieldD;  
     AND fieldE = ci_fieldE;  

   -- 処理部
  SET ci_fieldA = 11;
  SET ci_fieldB = 22;
  SET ci_fieldC = 33;
  SET ci_fieldD = 44;
  SET ci_fieldE = 55;
  -- カーソルにINPUTしたい変数すべてに値を設定

  OPEN cur;
  FETCH cur INTO co_field1, co_field2, 
        co_field3, co_field4, co_field5 .... -- カーソルから受け取る変数をすべて列挙
  CLOSE cur;

カーソル属性 %NOTFOUND等が使用できない。

次もカーソルに関する問題です。カーソルをループ内でFETCHするとき、すべての行を読み取ったかを判断するために、Oracleでは「カーソル名%NOTFOUND」属性によって判断します。MySQLでは、この「読み切った」ということをハンドラで判定します。

<Oracleの場合>

BEGIN
  -- カーソル定義
  CURSOR cur IS
    ... 
IS
  OPEN cur;
  LOOP
    FECH cur INTO rec;
    EXIT WHEN cur%NOTFOUND;  -- %NOTFOUNDを参照してループを抜ける

    ... (ループごとの処理)

  END LOOP;
  CLOSE cur;

<MySQLの場合>

BEGIN

  -- 変数定義
  DECLARE done INT DEFAULT FALSE;  -- ループ終了判定変数(初期値はFALSE)

  -- カーソル定義
  DECLARE cur CURSOR FOR
    ... 

  -- NOTFOUNDハンドラ 定義
  DECLARE CONTINUE HANDLER 
    FOR NOT FOUND SET done = TRUE;  -- NOTFOUND時にdoneにTRUEを設定

  OPEN cur;
  loop1 : LOOP
    FECH cur INTO rec;

    IF done THEN    -- doneがTRUEになったら処理終了
        LEAVE loop1;
    END IF;

    ... (ループごとの処理)

  END LOOP loop1;
  CLOSE cur;

こちらは先ほどのような機能不足ではなく、OracleMySQLの「仕様(お作法)の違い」ではありますが、地味に書き換えが面倒です。カーソルはPL/SQLの主要機能のため数が多く、ひとつのProcedureに複数のカーソルがあった場合には、それぞれを判定するための終了判定変数を作る必要がある(あるいはそれぞれをBEGINで括り、干渉を避ける)など、移行に手間がかかります。

その他

上記以外にも、二つのDBMSには組込み関数の名称の違いや、内部仕様の差異など、多くの違いがあります。

以下に一部をご紹介します。

概要 Oracle MySQL 移行時の注意点
NULL値判定の違い '(空)'もNULLもNULL扱い('' IS NULL = TRUE) '(空)'はNULLではない
('' IS NULL = FALSE)
入力値に応じて複数の条件式に分ける。
  IF (A='') OR (A IS NULL) THEN
これ以外にも、NVLやISNULLといったNULL判定に注意が必要である
DECODEが使えない DECODE(A, 1, 'yes', 'no'); CASE A WHEN 1 THEN 'yes', 'no' END 左記のとおり書き換える
配列データの取り回し VARRAY、連想配列、ネスト表などが使用できる 配列がないため、JSON型のリストを使用する
例 '[ 1, 2, 3 ...]'
データの加工・取出しには、MySQL組込み関数である「JSON関数」を使用する
NULL置き換え方法の違い NVL(abc, 0); IFNULL(abc, 0); 左記のとおり書き換える
文字列連結方法の違い data := a || b || c; SET data = CONCAT(a, b, IFNULL(c, '')); CONCATは一つでもNULLがあると結合結果がNULLとなるため、NULLが入りえる変数にはIFNULLと併用する。
再帰ルーチンの実装 できる できない 別途実装方法を検討

まとめ

今回、実際に移行を行ってみて、OraclePL/SQLの優秀さを改めて知るとともに、MySQLの非力さにうんざりしました。特に、カーソル制御まわりのI/Oがチープすぎで、古のCOBOLのようだと、最初は目を疑いました。引き渡し用の変数が相当数必要になるため、書き換えでミスが生まれそうで怖かったです。

こういったリプレース作業は、いつか、巷の生成AIがさくっとこなしてくれるようになるんでしょうね(遠い目)