Titelbild zum Artikel

Oracle LOB in Symfony2 und Doctrine2 mittels OCI8-Extension: Die Stolpersteine erkennen

Avatar von Michele Catalano

Oracle LOB in Symfony2 mit OCI-Lob Klasse nutzbar zu machen, ist nicht ganz einfach. Unter anderem die Unterstützung von Large Objects (LOB) hat in Doctrine2 einige Fallstricke, z.B. das Cachen von Queries, welches nicht ganz so optimal mit php_streams umgehen kann.

Hier will ich aber auf die Probleme bei der Benutzung des Oracle OCI8 Drivers von Doctrine2 in Bezug auf LOBs eingehen.

Das Problem

Das Hauptproblem der Nutzung vom Oracle OCI8-Treiber in Doctrine2 ist das eigene Datenmodell für das Streamen von LOBs. Dieses nennt sich OCI-Lob und hat alle nötigen Methoden für das Streamen dabei. Jedoch kann die Klasse von sich aus keinen php_stream liefern.

Deswegen wurde in Doctrine2 dieses Problem an zwei Stellen gelöst:

  1. In dem OCI8 Driver wird jedem oci_fetch_array und oci_fetch_all der Parameter OCI_RETURN_LOBS eingefügt. Diese Anpassung ist in der Klasse Doctrine\DBAL\Driver\OCI8\OCI8Statement zu finden. Der Parameter sagt dem Oracle-Treiber, dass alle LOBs als String und nicht als Stream geliefern werden. Hier das Snippet aus der Klasse:
    /**
     * {@inheritdoc}
     */
    public function fetch($fetchMode = null)
    {
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
        if ( ! isset(self::$fetchModeMap[$fetchMode])) {
            throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
        }
    
        return oci_fetch_array($this->_sth, self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
    }
  2. Es wird in dem Type BLOB immer ein Stream zurück an die Entity gegeben. Damit das garantiert ist, wird bei der Rückgabe eines Strings aus Queries, dieser per fopen() zu einem Stream gewandelt. Hier das entsprechende Code Snippet in der BlobType-Klasse:
    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if (null === $value) {
            return null;
        }
    
        if (is_string($value)) {
            $value = fopen('data://text/plain;base64,' . base64_encode($value), 'r');
        }
    
        if ( ! is_resource($value)) {
            throw ConversionException::conversionFailed($value, self::BLOB);
        }
    
        return $value;
    }

Das birgt in der Entwicklung eine interessante Stolperfalle: Ohne es zu wissen, wird jeder Blob als String im Speicher des PHP-Prozesses ablegt. Bei einer Query mit mehreren Rows kann das sogar dazu führen, dass auf einmal der Speicher des PHP-Prozesses vollläuft und man erst mal nicht weiß warum! (Reingelegt, kann man da nur sagen.)

Meistens ist es ja auch so, dass in der Entwicklung nur mit sehr kleinen Datenbeständen gearbeitet wird. Das führt oft dazu, dass das Problem erst in Produktion erkannt wird. Und das will man auf jeden Fall vermeiden! :-(

Damit ich mir das ganze Problem besser ansehen kann, habe ich in einer Oracle-Datenbank folgende Tabelle angelegt und mit Daten befüllt:

CREATE TABLE "FILES" (
  "ID" NUMBER(14,0) DEFAULT 0 NOT NULL ENABLE, 
  "CONTENT" BLOB DEFAULT '' NOT NULL ENABLE, 
  "FILESIZE" NUMBER(12,0) DEFAULT 0 NOT NULL ENABLE, 
  "MIME_TYPE" VARCHAR2(255 BYTE) DEFAULT 0 NOT NULL ENABLE, 
  "FILENAME" VARCHAR2(255 BYTE) DEFAULT 0 NOT NULL ENABLE, 
  CONSTRAINT "MO3_FILES_PK" PRIMARY KEY ("ID")
);

Eine mögliche Lösung

Das Ganze ist mit dem folgenden Ansatz gelöst worden:

  1. Mit einer stream_wrapper-Klasse für das OCI-Lob (es fehlt in diesem Beispiel noch eine Prüfung auf Fehlbenutzung): (auf StreamWrapperOci8Lob klicken, damit man denn Code sehen kann ;-) )
    <?php
    
    namespace Mayflower\Oci8TestBundle\Library;
    
    class StreamWrapperOci8Lob {
    
        /**
         * @var OCI-Lob
         */
        var $oci_lob_obj;
    
        /**
         * @var array
         */
        var $context;
    
        function stream_case($case_as)
        {
            return false;
        }
    
        function stream_open($path, $mode, $options, &$opened_path)
        {
            $url = parse_url($path);
            // TODO Fehler Prüfung und Check auf open mode.
            $context = stream_context_get_options($this->context);
            $this->oci_lob_obj = $context[$url['scheme']][$url['host']];
    
            return true;
        }
    
        function stream_read($count)
        {
            return $this->oci_lob_obj->read($count);
        }
    
        function stream_write($data)
        {
            return $this->oci_lob_obj->write($data);
        }
    
        function stream_tell()
        {
            return $this->oci_lob_obj->tell();
        }
    
        function stream_eof()
        {
            return $this->oci_lob_obj->eof();
        }
    
        function stream_seek($offset, $whence)
        {
            return $this->oci_lob_obj->seek($offset, $whence);
        }
    
        function stream_metadata($path, $option, $var)
        {
            return false;
        }
    
        function stream_stat()
        {
            $size = $this->oci_lob_obj->size();
            $now  = date("U");
    
            return [
                0         => 999, 
                1         => 0,
                2         => 33060,
                3         => 1,
                4         => 0,
                5         => 0,
                6         => -1,
                7         => $size,
                8         => $now,
                9         => $now,
                10        => $now,
                11        => -1,
                12        => -1,
                'dev'     => 999,
                'ino'     => 0,
                'mode'    => 33060,
                'nlink'   => 1,
                'uid'     => 0,
                'gid'     => 0,
                'rdev'    => -1,
                'size'    => $size,
                'atime'   => $now,
                'mtime'   => $now,
                'ctime'   => $now,
                'blksize' => -1,
                'blocks'  => -1,
            ];
        }
    }

    Damit man Funktionen, wie stream_get_contents auch auf diesen Stream anwenden kann, musste ich die stream_stat()-Methode implementieren. Hiermit werden als wichtigste Informationen die Größe und die Rechte von den Methoden zum Lesen des Streams entnommen. Die Timestamps sind hier einfach auf die aktuelle Zeit gesetzt, da sie im Prinzip auch die Entstehungszeit des Streams ist ;-)

  2. Der Driver und BlobType müssen noch überladen werden, damit der Wrapper genutzt werden kann. Anschließend muss der Driver noch kopiert und der Parameter OCI_RETURN_LOBS überall entfernt werden. Auch der BlobType muss kopiert und etwas stärker angepasst werden. Hier der angepasste Ausschnitt aus dem neuen BlobType:
    class OciBlobType extends Type
    {
    .....
        public function convertToPHPValue($value, AbstractPlatform $platform)
        {
            if (null === $value) {
                return null;
            }
    
            if (is_object($value) && get_class($value) == 'OCI-Lob') {
                if (!in_array('ocilob', stream_get_wrappers())) {
                    stream_wrapper_register("ocilob", "\\Mayflower\\Oci8TestBundle\\Library\\StreamWrapperOci8Lob");
                }
                $context = stream_context_create(['ocilob' => ['value' => $value]]);
                $value = fopen("ocilob://value", 'r', false, $context);
            }
    
            if (is_string($value)) {
                $value = fopen('data://text/plain;base64,' . base64_encode($value), 'r');
            } 
    
            if ( ! is_resource($value)) {
                throw ConversionException::conversionFailed($value, self::BLOB);
            }
    
            return $value;
        }
    .....
    }
  3. Um jetzt den neuen Driver und Typen nutzen zu können, müssen die noch in Symfony2/Doctrine2 registriert werden. Das Ganze wird per config.yml gemacht:
    doctrine:
        dbal:
            driver_class: Mayflower\Oci8TestBundle\Library\Doctrine\Driver\MyOCI8\Driver
            host:     "%database_host%"
            port:     "%database_port%"
            dbname:   "%database_name%"
            user:     "%database_user%"
            password: "%database_password%"
            charset:  UTF8
            persistent: true
            types:
                blob: Mayflower\Oci8TestBundle\Library\Doctrine\Types\OciBlobType

    Damit hat man in jeder Entity, welche den Type BLOB verwendet, eben jetzt wirklich einen Stream zur Verfügung.

Die Anpassung des BLOB-Typen ist insofern transparent, dass, wenn noch eine zweite Datenbankverbindung mit dem MySQL-Treiber geöffnet wird, diese mit dem Typen arbeiten kann.

Fazit

Im Standardfall von Symfony2 mit Doctrine für eine File-Liste aus einer Tabelle mit BLOB (ohne die Anzeige der BLOB-Daten):

  1. Speicherverbrauch des HTTP Requests ist 412 MB bei einer Gesamtgröße aller Blobs (Media Dateien) von 108,81 MB.
  2. Requestdauer beläuft sich auf 3183 ms.
Sample Anwendung ohne Doctrine OCI-Lob Nutzung. (Oracle LOB in Symfony2)
Bei der Benutzung der oben beschriebenen Implementierung sieht das ganze dann so aus:

  1. Speicherverbrauch des HTTP Requests ist 3.8MB bei einer Gesamtgröße aller Blobs (Media Dateien) von 108,81MB.
  2. Requestdauer beläuft sich auf 81ms.
Sample Anwendung mit Doctrine OCI-Lob Nutzung. (Oracle LOB in Symfony2)

Diese Beispiel basiert auf der standard Symfony Framework Edition mit Codeänderungen ohne Caching-Anpassungen oder weitere Änderungen.

Meiner Ansicht nach ist das ein sehr großer Fortschritt. Mit dem Stream ist es auch möglich, per StreamedResponse die Daten eines Bild-LOBs direkt an denn HTTP-Browser weiterzugeben, ohne damit den PHP-Prozess zu fluten.

Als ganz einfache Übersicht der Implementierung habe ich ein Beispiel unter https://github.com/Mayflower/symfony-doctrine-oci8-extension zur Verfügung gestellt.

Eine weitere Lösung existiert in der OCI8-Extension mit der OCI-LOB-Klasse, um einen php_stream wrapper zu erweitern. Leider hat das bis jetzt noch niemand gemacht, aber vielleicht findet sich ja noch ein Kandidat dafür.

Und wenn noch jemand eine viel coolere Idee hat, wie man dieses Problem lösen kann – über weitere Anregungen würde ich mich freuen.

Avatar von Michele Catalano

Kommentare

2 Antworten zu „Oracle LOB in Symfony2 und Doctrine2 mittels OCI8-Extension: Die Stolpersteine erkennen“

  1. Lesenswert: Oracle LOB in Symfony2 und Doctrine2 mittels OCI8-Extension: Die Stolpersteine erkennen http://t.co/xbiGgMjPAY

  2. Stolpersteine bei Oracle LOB in Symfony2 und Doctrine2 und der OCI8-Extension aus dem Weg räumen: https://t.co/A8KsKPPNW4 via @mayflowerphp

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert


Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.