ロゴ
HOME > CakePHP > CakePHP + Oracleにおける4,000バイト制限をどうにかする

CakePHP + Oracleにおける4,000バイト制限をどうにかする

2015年06月26日

以前も触れましたが、CakePHP + OracleでWEBシステムを構築する案件を進めています。

そもそもCakePHPはデフォルトでOracle用のドライバーが用意されておらず、有志が作成したものを使うしか無い状況に加え、date形式のカラムへの日時のインサートがそのドライバーでは対応出来ないという、厳しい状態です。

結局、単純な呼び出しはCakeを使う事が出来るものの(それでも日付は時間が取り出せない)、インサートやアップデートはモデルを使わずにsqlを走らせるしか無いようです。

例えば以下のような感じ

App::import('Model', 'ConnectionManager');

$db = ConnectionManager::getDataSource('default');
$sql = "UPDATE xxxxxx SET XXXX";
$result = $db->query($sql);

Cakeの縛りから開放されますので、複雑なjoinなども割と容易く実行出来ます。

さて、Oracleには発行するSqlに4,000バイトの制限が掛かっており、これを超えるとエラーとなります。
ORA-01704: 文字列リテラルが長すぎます

これがとても大変というか、そもそも日本語は2~4バイトあるので単純に2バイトとしても2,000文字、しかもsql文もこれに含まれるので、送信する文字列は更に少なくなります。

こういう場合はlob系を指定し、別な場所に格納する、という事らしい
http://www.shift-the-oracle.com/table/lob-storage.html

そもそも日本語はUnicode+PHPだと4バイトになる文字も有り、そうなるとフォームから送信出来るデータは、500文字程度にしないと安心出来ない。500文字って、ちょっとしたお問合せ内容なんかだと簡単に記入する量なので、そもそも無理があるのだと思います。

Googleなど大手でもMySQL(現在はmysql派生MariaDB)をチューニングして採用しているなど、WEBサービスではMysqlやPostgresの方が取り回しが楽なのです。

ですが、クライアントからcake+Oracleを指定されている以上はどうしようも有りません。

今回のテーブルの構造と制約

テーブルを変更する際に、それを上長が許可する事で実行される、いわゆるワークフローが必要で、新旧データを格納するテーブルが必要になります。更新がいつ行われ、どのデータをどう変更したか、誰が承認したかなどを記録する為のテーブルです。

また、これには個人情報が含まれる可能性が有り、暗号化も必要になります。但し、用意してくれたOracle暗号化classは単純な同一文字列での暗号化で、rijndael-256のようなものではなく(これはパッケージ会社の指定)、ちょっと怖い。ですので、cakeで暗号化して保存します。

Crypt.php

暗号化用の共用classを作成するため、Vendorあたりに適当なファイルを作ります。

class Crypt {
    static public function encrypt($text) {
        return Security::rijndael($text, Configure::read('Security.salt'), 'encrypt');
    }

    static public function decrypt($text) {
        return Security::rijndael($text, Configure::read('Security.salt'), 'decrypt');
    }
}

beforeSave

    public function beforeSave($options = array()) {
     //新データ 暗号化
      if (isset($this->data[$this->alias]['new'])) {
        $value  = gzdeflate( $this->data[$this->alias]['new'] , 9); //圧縮
        $value  = Crypt::encrypt($value); //暗号化
        $value  = base64_encode($value);  //base64
        
        $this->data[$this->alias]['new'] = $value;
      }
     //旧データ 暗号化
      if (isset($this->data[$this->alias]['old'])) {
        $value  = gzdeflate( $this->data[$this->alias]['old'] , 9); //圧縮
        $value  = Crypt::encrypt($value); //暗号化
        $value  = base64_encode($value);  //base64
        
        $this->data[$this->alias]['old'] = $value;
      } 
    }

該当モデルのbeforeSaveで、新データ(new) 旧データ(old) の処理を行います。

1) gzdeflateで文字列を圧縮する
2) Crypt::encryptで暗号化
3) base64_encode

AppModel

データを呼び出すとき、そのままでは意味不明な文字列ですので、AppModelへ復号化などを行う処理を書きます。

class AppModel extends Model {
    public function afterFind($results, $primary = false) {
        foreach ($results as $key => $val) {
            if (isset($val['MODEL']['new'])) {
              $value = base64_decode($val['MODEL']['new']);
              $value = Crypt::decrypt($value);
              $value = gzinflate($value);

              $results[$key]['MODEL']['new'] = $value;
            }
            if (isset($val['MODEL']['old'])) {
              $value = base64_decode($val['MODEL']['old']);
              $value = Crypt::decrypt($value);
              $value = gzinflate($value);

              $results[$key]['MODEL']['old'] = $value;
            }
        }
        return $results;
    }
}

暗号化のときの逆をやる、という事です。

当初はシリアライズするなど、色々手を考えてみたのですが、どちらにしても文字列が多くなれば4,000バイトに引っかかってしまうので、これまであまり使う機会の無かったgzinflateを使う事で圧縮しました。

gzinflateそのものは日本語でテストすると、圧縮比9でだいたい1/3程度に圧縮されるようです。

だいたい50カラム程度のテーブルの新旧履歴を格納しても大丈夫なので、テキストエリアをある程度の文字数に制限すれば、よほどの事が無い限り大丈夫そうです。

 

Oracleを使ったWEBシステム構築の経験が有る、とは言えない程度しか経験が無いのですが、改めて勉強をしつつ開発を進めると、どうにもこうにも扱いづらい。

オラクルマスターには最高のRDBで、これ以外は無いと思えるほど素晴らしいのでしょうが、WEBベースで考えると正直、使えない子としかまだ思えません。ましてそれをCakePHPという縛りの中でやるのは、指定が無い限りやめた方が良いと思われます・・・Symphonyやzendあたりでもおそらくこの解答になりそうな予感