В моей компании есть проект, который построен на Yii 1.1 и использует базу данных Oracle 9g. Для работы с базой используется расширение oci8Pdo.

Недавно появилась задача загрузки сканов в базу в поле BLOB. Т.к. автор расширения пишет:
The goal of this PDO class is to simulate 99% of the PDO functions that you use in an application.
то причин сомневаться в реализации данного функционала было мало.

Пробуем загрузить скан:

<?php
    $doc_scan = file_get_contents($file);
    $db = Yii::app()->dbOracle;
    $stmt = $db->createCommand("update scan_document set DOCUM_SCAN=:doc_xml, DOC_SCAN=:doc_scan where DOCUM_ID=:docum_id");
    $stmt->bindParam(':doc_xml', $doc_xml, PDO::PARAM_LOB);
    $stmt->bindParam(':doc_scan', $doc_scan, PDO::PARAM_LOB);
    $stmt->bindValue(':docum_id', $add->DOCUM_ID);
    $stmt->query();
?>

Но не тут-то было: ORA-01465: invalid hex number.

Стали раскуривать проблему и наткнулись на реализацию метода bindParam в классе Oci8PDO_Statement вышеописанного расширения:

<?php
public function bindParam(
    $parameter,
    &$variable,
    $data_type = PDO::PARAM_STR,
    $length = -1,
    $driver_options = null
) {
	//Not checking for $data_type === PDO::PARAM_INT, because this gives problems when inserting/updating integers into a VARCHAR column.
    if ($driver_options !== null) {
        throw new PDOException('$driver_options is not implemented for Oci8PDO_Statement::bindParam()');
    }
    
    if (is_array($variable)) {
        return oci_bind_array_by_name(
            $this->_sth,
            $parameter,
            $variable,
            count($variable),
            $length
        );
    } else {
        if ($length == -1) {
            $length = strlen((string)$variable);
        }

        return oci_bind_by_name($this->_sth, $parameter, $variable, $length);
    }
}
?>

Аргумент $data_type принимается, но нигде не обрабатывается. Так и получается что CLOB или BLOB записать у нас не выйдет. Деваться было некуда, пришлось допиливать oci8Pdo.

В класс Oci8PDO который наследуется от PDO добавили константы и метод вытаскивающий ресурс подключения к ДБ:

<?php
	/**
     * Ananalog constant OCI_B_CLOB
     *
     * @const int
     */
    const PARAM_CLOB = 112;
    /**
     * Ananalog constant OCI_B_BLOB
     *
     * @const int
     */
    const PARAM_BLOB = 113;


    // ..............


    /**
     * Return the resource connection
     *
     * @return mixed
     */
    public function getDbh() {
        return $this->_dbh;
    }
?>


И немного допилили метод bindParam в классе Oci8PDO_Statement:

<?php
	public function bindParam(
        $parameter,
        &$variable,
        $data_type = PDO::PARAM_STR,
        $length = -1,
        $driver_options = null
    ) {
    	
    	// ................

    	if ($data_type == Oci8PDO::PARAM_BLOB) {
            $clob = oci_new_descriptor($this->_pdoOci8->getDbh(), OCI_D_LOB);
            $res = oci_bind_by_name($this->_sth, $parameter, $clob, -1, OCI_B_BLOB);
            $clob->writeTemporary($variable, OCI_TEMP_BLOB);
            return $res;
        } else if ($data_type == Oci8PDO::PARAM_CLOB) {
            $clob = oci_new_descriptor($this->_pdoOci8->getDbh(), OCI_D_LOB);
            $res = oci_bind_by_name($this->_sth, $parameter, $clob, -1, OCI_B_CLOB);
            $clob->writeTemporary($variable, OCI_TEMP_CLOB);
            return $res;
        }
        else {
            return oci_bind_by_name($this->_sth, $parameter, $variable, $length);
        }

    }
?>

Теперь обработка CLOB/BLOB проходит успешно:

<?php

$doc_scan = file_get_contents($file);
$db = Yii::app()->dbOracle;
$stmt = $db->createCommand("update scan_document set DOCUM_SCAN=:doc_xml, DOC_SCAN=:doc_scan where DOCUM_ID=:docum_id");

$stmt->bindParam(':doc_xml', $doc_xml, Oci8PDO::PARAM_CLOB); // Используем наши константы
$stmt->bindParam(':doc_scan', $doc_scan, Oci8PDO::PARAM_BLOB); // Используем наши константы
$stmt->bindValue(':docum_id', $add->DOCUM_ID);
$stmt->query();

?>

Итог:

Допил был организован в пулреквест и отправлен разработчику oci8Pdo yjeroen. Когда гуглилась проблема, было замечено много нерешенных вопросов на эту тему. Надеюсь, мой опыт кому-нибудь поможет.

P.S.: Буду рад критике и конструктивным замечаниям в комментах.

Комментарии (6)


  1. sparksounds
    24.09.2015 12:21
    +1

    php / oracle это странная связка, очень много проблем огреб. Довелось начинать проект с oracle в качестве базы, с планами миграции на mysql в дальнейшем. Единственный ORM который осилил oracle — propel orm. Благо этап oracle пройден, данные успешно перенесены в mysql.


    1. oledje
      24.09.2015 13:19
      +1

      Тоже довелось немало использовать связку PHP и Oracle (причем именно с Yii 1). Почему-то этот стек любят гос. структуры в России для разработки своих приложений. Да и еще на Windows / IIS все это дело крутится. Проблемы конечно были, например с Prepared statements. Бывало при передаче параметров запрос мог выполняться в десятки(!) раз медленнее чем при прямой вставке значений.

      Но в целом могу сказать что дело привычки. Просто нужно научиться его готовить.


      1. SamDark
        24.09.2015 15:13

        Откаты…


      1. Mur466
        24.09.2015 18:57

        Бывало при передаче параметров запрос мог выполняться в десятки(!) раз медленнее чем при прямой вставке значений.

        Это нормальное поведение Oracle независимо от типа клиента. Так работает стоимостной оптимизатор запросов Oracle.

        Если вы передаете значения литералами, то Oracle строит план запроса каждый раз, при этом построение плана запроса — довольно затратная операция и для многих запросов может занимать больше времени чем собственно выборка данных.
        Поэтому для уменьшения общей нагрузки на сервер рекомендуется использовать bind-переменные. В таком случае Oracle анализирует значения переменных первый раз, и строит план, а при дальнейших вызовах идентичного запроса с любыми значениями переменных (даже из других сессий) использует ранее созданный план.

        Но это не всегда оправдано.
        Предположим вы делаете запрос по большой (миллионы записей) таблице заказов с условием по двум полям — customer_id и order_date в диапазоне. Количество уникальных клиентов — десятки тысяч, история заказов за много лет. У вас есть индекс в таблице и по каждому из полей — client_id и order_date.

        При первом вызове вы передали диапазон дат в 1 день, и client_id клиента с очень большой историей заказов. Оптимизатор проанализировал статистику данных и решил использовать индекс по дате, так как выгоднее перебрать все заказы за 1 день, чем все заказы клиента за много лет. Такой план сохранился в кеше запросов Oracle.

        При одном из последующих вызовов вы передали в bind-переменные диапазон дат длинной в год и другой client_id. И хотя у этого клиента за всю историю всего несколько заказов, используется все равно индекс по дате. В итоге перебираются все заказы всех клиентов за год (сотни тысяч записей), что занимает в десятки раз больше времени, чем перебор всех заказов (за всю историю) этого конкретного клиента. Если бы параметры передавались литералами, то план запроса был бы создан заново, использовался бы индекс по client_id, и конечно такой запрос выполнился бы во много раз быстрее.

        До 10-й версии данная проблема стояла довольно остро, в более поздних версиях были попытки как-то эту проблему решить.


  1. not_ice
    24.09.2015 15:56

    А phppdo не пробовали? Он умеет clob/blob в Oracle. Либа старая, но с yii 1.1 работает нормально.
    Хотя у вас вроде тоже нормально получилось.


    1. sfedosimov
      26.09.2015 18:24

      Не всегда есть возможность использовать PDO, например у нас php 5.2.17 где драйвер для oracle в экперементальном виде. Были проблемы с передачей больших xml в процедуры oracle с PDO.