Oracle SQL és Yii Framework kompatibilitás

Szerző: Borsos Albert 2015-04-20 11:18 Szólj hozzá!

Nagyvállalati környezetben elég népszerű az Oracle. Viszont a Yii Framework támogatása nem a legjobb az Oracle adatbázisokhoz. Mondhatni borzalmas, de néhány trükkel meg lehet oldani, hogy kellemesebb legyen a fejlesztés. Ezeket gyűjtöttem össze, amikbe eddig beleszaladtam.

Ezek tipikus hibák a Yii keretrendszerben. A fejlesztés során kollégáim is találkoztak ugyanezekkel a bug-okkal. Reméljük a Yii 2.0-ban már lesz rá rendes támogatás.

Először is létrehoztam egy OracleActiveRecord.php osztályt, ebbe pakolom bele az Oracle specifikus dolgokat. Mivel túl sok minden nem kerül bele ezért a következő kódrészletbe szerepel az egész osztály tartalma.

DATE és DATETIME értékek mentése

Ez volt az első amivel találkoztam. Az Oracle adatbázisnak át kell konvertálni a date és datetime típusú attribútumainkat, mivel egy űrlapról elküldve string-ként kerülnek bele a modellünkbe. Mivel ezt elég gyakran használom és elég csúnya a kód, ezért beburkoltam 1-1 metódusba.

<?php
/**
 * Description of OracleActiveRecord
 *
 * @author borsosalbert
 */
class OracleActiveRecord extends CActiveRecord
{
    /**
     * CLOB típusú attribútumok értékét ebbe a tömbbe kell menteni a beforeSave()-ben
     * - majd az attribútum értékét null-ra állítani
     * - majd az afterSave()-ben update-elni a rekord CLOB attributumának értékét
     */
    public $tmp = array();
    /**
     * átalakítja a dátumot oracle-ben tárolható formátumra
     */
    public function convert_date_before_save($date, $format = 'YYYY-MM-DD hh24:mi:ss')
    {
        return new CDbExpression("to_date('" . $date . "','{$format}')");
    }
    /**
     * oracle dátumobjektumot visszaalakítja string-gé, hogy be lehessen tölteni input mezőbe
     */
    public function convert_date_after_save($date, $format = 'YYYY-MM-DD hh24:mi:ss')
    {
        return str_replace(array("to_date('", "','{$format}')"), '', $date->expression);
    }
}

Röviden összefoglalva arra jó, hogy ha egy űrlapon be lehet állítani egy mezőnek dátum értéket mondjuk egy datepicker-rel, akkor beforeSave() metódusban a convert_date_before_save()-et kell meghívni, így az adatbázisba el lehet menteni. Majd rögtön az afterSave()-ben meg kell hívni a convert_date_after_save() metódust, hogy az űrlapra visszatöltse a mezőbe az értéket.

Oracle CLOB mezők Yii-ben

Igyekeztem a lehető legszebb módon összehozni, nézzük mi lett belőle :) Amit tudni kell, hogy Oracle adatbázisokban MySQL-el ellentétben nincs olyan mezőtípus, hogy text. Van viszont VARCHAR2(4000), ami 4000 karakter tárolására képes. ISO-8859-2-ben :D UTF-8 esetén már csak 1333 karakter, mert 1 karaktert 3 byte-on tárol. 1333 karakterbe pedig nem fér túl sok minden ezért CLOB-ot kell használni helyette.

Ez még nem tűnik vészesnek, de mikor egy egész adatbázisszerkezetet elkészítettél VARCHAR2(4000)-es mezőkkel és miután rájöttél, hogy ez kevés, na akkor már nem olyan kis munka átvariálni az egész adatmodelt. És itt még nincs vége.

Ha lekérdezel egy rekordot egy táblából, akkor a CLOB mezőket Resource típusúként kapod meg. ezt viszonylag könnyen lehet kezelni, ha objektumként kapod vissza a rekordot. A következő példában a DESCRIPTION mező típusa CLOB:

protected function afterFind()
{
    parent::afterFind();
    if (!is_null($this->DESCRIPTION) && is_resource($this->DESCRIPTION)){
        $this->DESCRIPTION = stream_get_contents($this->DESCRIPTION);
    }
}

Ezt már nem az OracleActiveRecord.php-ban kell elhelyezni, hanem annak egy példányában, a protected/models alatt. Így ha Table::model()->findByPk(1)-el kérdezel le egy modelt, akkor használható módon kapod vissza a CLOB mezőt, mintha sima szöveg lenne.

Van még egy csavar (persze nem az utolsó). Ha Table::model()->findAll()-al kérdezel le több rekordot, és egy foreach ciklusban szeretnéd kigenerálni (vagy egy CGridView-ban), akkor azzal fogsz szembesülni, hogy mindegyik mező értéke ugyanaz lesz, méghozzá az utolsó rekord mezőjének értéke. És ha ez pont null érték, akkor törheted az agyad, hogy mi a fenéért nem jeleníti meg, hiszen ha csak egyet kérsz le, akkor jó. Na erre a megoldás az, hogy a foreachben újra le kell kérdezni a CLOB mezők értékét. Persze performanciának jót tesz. De ez még mind semmi!

A mentési procedúra mégszebb. A kommentelt részben ugyan kicsit spoilereztem, de itt kifejtem bővebben. Ha model-ben dolgozol, akkor nem fogod tudni elmenteni a CLOB mezők értékét egy $model->save()-vel. Azért, mert a CLOB mezők értékét bind-olni kell, amit model-ben nem lehet. Így kerül képbe a $tmp változónk, ami egy tömb lesz, hogy egyszerűbben tudjunk vele dolgozni több attribútummal is.

A modellünk beforeSave() metódusában elmentjük a $tmp-be az attribútumunk értékét és null-t adunk a model attribútumában lévőnek, így műkdöni fog a save() metódusunk. Előző példából kiindulva maradjunk a DESCRIPTION attribútumnál.

protected function beforeSave()
{
    if (parent::beforeSave()) {
        $this->tmp['DESCRIPTION'] = $this->DESCRIPTION; // elmentjük a tartalmát
        $this->DESCRIPTION = null; // null-ázzuk a model-ben
        return true;
    } else {
        return false;
    }
}

Persze ezzel még nem jutottunk semmire, hiszen az adatbázisba null érték szerepel. Ezért a $tmp-ből egy UPDATE utasítással frissítjük. Először le kell kérdeznünk az elem azonosítóját. Ezt új rekord és módosításnál másképp kell megoldani, ezért van az elején egy feltétel. Majd ha megvan az id-nk akkor egy SQL utasítással frissítjük. Persze a CLOB mezőt bind-olni kell úgy, hogy a bindParam() metódus 3. paraméterének a PDO::PARAM_STR-t adjuk meg, a 4-nek pedig az attribútumunk hosszát. Majd a model attribútumának adjuk vissza a $tmp-ben lévő tartalmat, hogy az űrlapra visszatöltse. Ha több CLOB attribútumunk van, akkor azokkal is meg kell ezt tenni.

protected function afterSave()
{
    parent::afterSave();
    if ($this->isNewRecord) {
        $subquery = 'SELECT ID FROM '.$this->tableName().' ORDER BY ID DESC';
        $sql      = "SELECT * FROM ({$subquery}) WHERE ROWNUM < 2";
        $id       = Yii::app()->db->createCommand($sql)->queryScalar();
    }else{
        $id = $this->getPrimaryKey();
    }
    $sql = 'UPDATE '.$this->tableName().' SET'
            . ' DESCRIPTION=:description'
            . ' WHERE ID=:id';
    $command = Yii::app()->db->createCommand($sql);
    $command->bindParam(':description', $this->tmp['DESCRIPTION'], PDO::PARAM_STR, strlen($this->tmp['DESCRIPTION']));
    $command->bindParam(':id', $id);
    $command->execute();
    $this->DESCRIPTION = $this->tmp['DESCRIPTION'];
}

Sajnos ennél szebben nem lehetett megoldani. Ha le lehetne kérdezni Yii-ben, hogy melyik attribútumok CLOB típusúak, akkor az OracleActiveRecord-ban is le lehetne kezelni, de így kénytelenek vagyunk minden modellünkben egyenként leprogramozni.

Ha hasznosnak találtad, vagy találkoztál mással is, ami Oracle-Yii specifikus bug, akkor ne tartsd magadban!

 
A hozzászólások a Disqus segítségével jöttek létre