Capítulo 5. Fuentes de datos

Tabla de contenidos

5.1. Conexiones BBDD
5.1.1. Bases de Datos accesibles por gvHidra
5.1.2. Acceso y conexión con la Base de Datos
5.1.3. Transacciones
5.1.4. Procedimientos almacenados
5.1.5. Recomendaciones en el uso de SQL
5.2. Web Services
5.2.1. Web Services en PHP
5.2.2. Generación del WSDL
5.2.3. Web Services en gvHIDRA
5.3. ORM
5.3.1. Introducción
5.3.2. Ejemplo: Propel ORM

5.1. Conexiones BBDD

5.1.1. Bases de Datos accesibles por gvHidra

Una de las características importantes que tenemos en PHP es la gran cantidad de distintos tipos de bases de datos soportados. Cada una de ellas requiere de una extensión que define su propia API para realizar la misma tarea, desde establecer una conexión hasta usar sentencias preparadas o control de errores. Cambiar las llamadas a la API cuando cambiamos de BD en una aplicación es costoso pero se puede hacer, aunque en el caso de un framework no es factible porque tiene que funcionar el mismo código con varias BD. Lo que se suele hacer es añadir una capa de abstracción que nos permita operar de la misma manera con todos los tipos de BD (al menos para las operaciones más habituales o las más estandarizadas). En el caso de gvHIDRA se utiliza la librería MDB2 de PEAR (http://pear.php.net/package/MDB2/ ), una de las más usadas para este fin gracias a su amplia funcionalidad y a la actividad del proyecto. De esta forma podemos usar una BD sin preocuparnos por el tipo, ya que tenemos una API única.

Sin embargo, a pesar de usar la capa de abstracción, sigue habiendo aspectos que dependen del tipo de BD: uso de características especiales (secuencias, limit, bloqueos, ...), manejo de transacciones, representación de fechas y números, ... Para resolver estos problemas, definimos en el framework otra capa de abstracción (que se implementa en unas clases que se ven en el apartado siguiente) y las funciones más importantes que realizan son:

  • Definición del formato de fechas y números que se va a usar para comunicarse con el gestor de BD. Este formato es transparente para el programador, ya que es el framework el que hace las conversiones necesarias cuando enviamos/recogemos información a/de la BD. Normalmente nos referiremos a éste como formato de BD.

  • Inicialización de la conexión: son operaciones que se hacen cada vez que se hace una nueva conexión. Por ejemplo se fija la codificación que se va a usar en la comunicación (actualmente LATIN1 o ISO-8859-1, que es la usada en el framework), o se modifican parámetros de la conexión para establecer el formato de BD.

  • Ajustes por sintaxis: aquí se incluyen una serie de métodos para obtener la sintaxis de ciertas operaciones que se pueden comportar de forma distinta en cada BD. Por ejemplo la clausula limit (para paginar resultados de consultas), función para concatenar cadenas, uso de secuencias, ...

  • Unificar tratamiento de errores: por ejemplo, cuando hay un registro bloqueado, poderlo detectar independientemente de la BD.

Actualmente las BD soportadas por gvHIDRA son postgresql, mysql y oracle. Conforme vayamos soportando nuevos tipos (entre los soportados por MDB2), la funcionalidad de esta capa se podrá ir aumentando.

5.1.2. Acceso y conexión con la Base de Datos

Cómo crear conexiones a distintas fuentes de datos.

5.1.2.1. Introducción

El framework ofrece una serie de facilidades al programador que pueden llegar a ocultar las posibilidades que tiene de conexión a BBDD. Concretamente, tanto el pattern gvHidraForm_DB o el debug (IgepDebug), son ejemplos de uso de conexiones de forma transparente para el programador. En ambos casos, gvHidra se encarga de conectar con el SGBD correspondiente y operar con él, dejando una serie de métodos que para consultar/operar.

Evidentemente, puede que nos encontremos con circunstancias que nos obliguen a utilizar algo más de lo que hemos expuesto. Para cubrir todo ello, el framework ofrece la clase IgepConexion que, a través de la librería PEAR::MDB2 conecta con diferentes SGBD. Esto permite crear conexiones a diferentes fuentes de forma homogenea e independizárnos de los drivers nativos que trabajan por debajo.

A esta capa de abstracción, el framework aporta una serie de perfiles de SGBD (IgepDBMS) que nos permiten configurar dichas conexiones para, por ejemplo, independizar el formato de la configuración del SGBD. Actualmente se han definido los siguientes perfiles:

Tabla 5.1. Perfiles SGBD

SGBD PERFIL (CLASE)
Oracle IgepDBMS_oci8.php
PostgreSql IgepDBMS_pgsql.php
MySQL IgepDBMS_mysql.php

Nota

se pueden crear perfiles para otros SGBD de forma sencilla.

5.1.2.2. Creación de una conexión

Para crear una conexión a una base de datos simplemente tenemos que crear una instancia de la clase IgepConexion pasándole el dsn de conexión a dicha BBDD. Dicho dsn tiene que tener la siguiente estructura (en PHP):

//Ejemplo postgresql
$dsnPos = array(
    'phptype'  => 'pgsql',
    'username' => 'xxxx',
    'password' => 'xxxx',
    'hostspec' => 'xxxx',
    'database' => 'xxxx',
);            

//Ejemplo oracle
$dsnOra = array(
    'phptype'  => 'oci8',
    'username' => 'xxxx',
    'password' => 'xxxx',
    'hostspec' => 'xxxx',
);

Estos dsn se tienen que definir en el fichero de configuración de la aplicación gvHidraConfig.inc.xml siguiendo la sintaxis que especifica su DTD. También se pueden definir en la carga dinámica, aunque es poco recomendable.

La construcción de la conexión sería tan simple como:

//Nueva conexion a postgresql
$conPos = new IgepConexion($dsnPos);

//Nueva conexion a oracle
$conOra = new IgepConexion($dsnOra);

Después de crear la instancia, hay que comprobar si ha habido problemas de conexión:

if (PEAR::isError($conPos->getPEARConnection()))
     return;

¿Conexiones persistentes?

Si, conexiones persistentes aunque con matizaciones. El driver nativo de PHP para PostgreSQL ofrece la posibilidad de, con el objeto de optimizar, reutilizar las conexiones. De modo que, si creamos 10 conexiones, realmente tenemos 10 referencias a la misma conexión. Esto aumenta el rendimiento ya que sólo tenemos un "coste" de conexión aunque, evidentemente es peligroso. Por ello el framework, por defecto, crea conexiones independientes que mueren cuando el programador destruye el objeto (o cuando acaba el hilo de ejecución).

Puede que, en algunos casos puntuales, se quiera utilizar este tipo de conexiones (sólo funcionarían en PostgreSQL), y para esos casos se ha creado un parámetro en el constructor de la conexion (bool). Este parámetro, persistent, indica si el programador quiere solicitar una conexion persistente, siendo su valor por defecto false.

//Conexion permanente (reusable/reusada)
$pcon = new IgepConexion($dsn_log,true);

5.1.2.3. Utilizar conexión de la clase manejadora (gvHidraForm_DB)

Puede que en ciertas circunstancias nosotros queramos recuperar/utilizar la conexión que mantiene automáticamente la clase manejadora (las que extienden de gvHidraForm_DB). Para ello, se facilita el método getConnection que devuelve la instancia de la conexión en uso.

//En la clase manejadora
$con = $this->getConnection();

Nota

la clase manejadora ofrece también los métodos consultar y operar sobre esta conexión para poder lanzarlos sin recuperar la instancia de la conexión.

5.1.2.4. Métodos de conexión

En este apartado vamos a ver algunos de los métodos interesantes que ofrece la clase de conexiones de gvHIDRA. No los vermos todos (para ello consultar PHPDoc o doxy), pero si los más útiles.

5.1.2.4.1. consultar

Este método sirve para realizar una query con la conexión activa. El primer parámetro corresponde a la SELECT que queremos ejecutar. Opcionalmente podemos pasarle un segundo parámetro indicando si queremos que transforme los datos (especialmente fechas y números) al formato interno del framework. Sino se especifica este segundo parámetro, los datos vendrán en el formato configurado para cada SGBD, por lo que puede que no sean adecuados para operar con ellos.

// obtenemos datos para operar en capa de negocio (FW)
$res=$con->consultar(" SELECT fecha_ini,fecha_fin
         		 FROM tper_permisos
         		 WHERE nregpgv = '44908412R'", 
                     array( 'DATATYPES'=>array('fecha_ini'=>TIPO_FECHA,'fecha_fin'=>TIPO_FECHA,)));
5.1.2.4.2. operar

Este método sirve para lanzar operaciones DML más allá del comando SELECT; es decir UPDATE,INSERT o DELETE. En ocasiones necesita que se ejecute sobre los datos el método prepararOperacion.

5.1.2.4.3. prepararOperacion

Convierte los campos al formato que acepta la base de datos (escapar los carácteres especiales en textos, ajustar los separadores decimales y de grupos en números y formatos en fechas). Hay que llamarla antes del método consultar, operar o preparedQuery. Los parámetros que acepta son:

  1. valor: puede ser un valor simple o un array de registros, donde cada registro es un array con estructura 'campo'=>valor. Es un parámetro por referencia, por lo que devuelve el resultado en esta variable.

  2. tipo: si el valor es simple indicaremos directamente el tipo que puede ser las constantes: TIPO_CARACTER, TIPO_ENTERO, TIPO_DECIMAL, TIPO_FECHA y TIPO_FECHAHORA. Si el valor es un array, el tipo contiene un array asociativo con los tipos de cada columna. Más adelante se puede ver un ejemplo con valor simple, y éste seria un ejemplo con array:

    $datos = array( array('nombre'=>"L'Eliana", 'superficie'=>'45.6') );
    $conexion->prepararOperacion($datos, array('nombre'=>array('tipo'=>TIPO_CARACTER,), 'superficie'=>array('tipo'=>TIPO_DECIMAL,),));
    // en $datos ya tenemos los datos transformados
5.1.2.4.4. CalcularSecuenciaBD

Calcula el valor de una secuencia de BD. Consulte en el manual de su SGBD las opciones que ofrece al respecto.

5.1.2.4.5. CalcularSecuencia

Calcula el máximo de una tabla siguiendo varios criterios (según los parámetros).

5.1.2.4.6. preparedQuery

Es similar a consultar y operar, pero usando sentencias preparadas. Es más eficiente que estar formando cada vez la sentencia SQL ya que el SGBD solo tiene que analizar la sentencia la primera vez. No requiere escapar caracteres especiales. Es recomendable especialmente cuando usamos la sentencia dentro de bucles. Al igual que consultar, también tiene un parámetro opcional, que permite pasarle los tipos de los campos. El último parámetro también es opcional y sirve para conservar la sentencia preparada cuando ejecutamos la misma sentencia varias veces con distintos parámetros.

A continuación ponemos un ejemplo de utilización. Comprobamos que, dadas las facturas del año 2006, todas sus líneas esten en estado 'REVISADA', y si no es así actualizamos la factura en concreto pasándola a estado 'SINREVISION' y le añadiremos el campo observaciones que ha introducido el usuario.

$conexion = new IgepConexion($dsn);
$sel = "SELECT nfactura as \"nfactura\" FROM lineas WHERE anyo='".2005."' AND estado<>'REVISADA' group by nfactura";
$res = $conexion->consultar($sel);
if ($res!=-1 and count($res[0])>0) {
   //Como no sabemos el contenido de observaciones escapamos por si hay cualquier caracter problematico
   $conexion->prepararOperacion($observaciones, TIPO_CARACTER);
   foreach ($res as $facturaSinRevision){
      $res2 = $conexion->operar("UPDATE factura set observaciones='$observaciones', estado='SINREVISION' 
                                WHERE anyo='2005' AND factura='".$facturaSinRevision['nfactura']."'");
      if ($res2==-1) {
         // Mensaje de Error
         ....
      }
}

El mismo ejemplo usando sentencias preparadas seria:

$conexion = new IgepConexion($dsn);
try {
   $sel = "SELECT nfactura as \"nfactura\" FROM lineas WHERE anyo=? AND estado<>'REVISADA' group by nfactura";
   $res = $conexion->preparedQuery($sel, false, array(2005));
   if (count($res[0])>0) {
      $upd = null;
      $sql = "UPDATE factura set observaciones=?, estado='SINREVISION' WHERE anyo=? AND factura=?";
      //con sentencias preparadas no es necesario escapar caracteres especiales
      foreach ($res as $facturaSinRevision)
         $res2 = $conexion->preparedQuery($sql, true, array($observaciones,'2005',$facturaSinRevision['nfactura']), null, $upd);
   }
} catch (Exception $e) {
   // Mensaje de Error
   ...
}

El control de errores con sentencias preparadas siempre es a través de excepciones.

También están disponibles los métodos consultarForUpdate y preparedQueryForUpdate que permiten bloquear los registros que recuperan. Más información aquí.

5.1.2.5. Utilizar el driver MDB2 o nativo

En algunos casos, puede ser interesante trabajar directamente con el driver PEAR::MDB2 o el driver nativo de PHP. Un ejemplo puede ser la utilización de procedimientos almacenados. Para estos casos el FW ofrece el método getPEARConnection que devuelve el objeto PEAR::MDB2 conectado

/*CONEXION ACTIVA DE LA CLASE MANEJADORA*/
//Recogemos el objeto IgepConexion de la conexion activa.
$conPEARMDB2 = $this->getConnection();
//Recogemos el objeto PEAR::MDB2 de la conexion activa.
$conPEARMDB2 = $this->getConnection()->getPEARConnection();
//Recogemos el driver nativo de PHP de la conexion activa.
$conPHP = $this->getConnection()->getPEARConnection()->getConnection();

/*NUEVA CONEXION*/
//Recogemos el objeto PEAR::MDB2
$this->_conOracle = new IgepConexion($g_dns_ora);
$conPEARMDB2 = $this->_conOracle->getPEARConnection();
//Recogemos el driver nativo de PHP
$conPHP = $this->_conOracle->getPEARConnection()->getConnection();

5.1.3. Transacciones

5.1.3.1. Introducción

Actualmente el framework no fija el parámetro autocommit, por lo que estamos usando el valor fijado en los servidores: activado para postgres y mysql, y desactivado para oracle (de hecho, no se puede activar). En próximas versiones se normalizará esta situación. Cuando queramos englobar varias operaciones en una transacción, tendremos que iniciar una transacción o asegurarnos que ya estamos en una. De momento no hay soporte para transacciones anidadas.

Nota

En mysql, para tener soporte transaccional hay que definir las tablas con storages que lo soporten como InnoDB.

El framework inicia una transaccion automaticamente para hacer las operaciones del CRUD. Para el resto de casos, deberemos iniciar ésta de modo explícito. Se puede hacer con el método empezarTransaccion de IgepConexion. En las clases de negocio no hace falta hacerlo (ya que el framework inicia la transacción), a menos que queramos que la transacción empiece en preInsertar, preModificar o preBorrar.

Ejemplo:

$this->getConnection()->empezarTransaccion();

También es habitual empezar una transacción cuando tenemos una conexión sobre un DSN distinto al empleado en el formulario, sobre el que queremos hacer operaciones de actualización.

La transacción acaba con el método acabarTransaccion, y recibe un parámetro booleano indicando si ha habido error, y en función de éste se hace un commit o rollback. No hay que llamarlo cuando se trata de la transacción del framework.

5.1.3.2. Control de Concurrencia

Para las operaciones del CRUD, el framework aplica un método simple para evitar que dos usuarios modifiquen simultáneamente un registro que previamente han leído. Consiste en añadir a la condición del where, en los update y delete, todos los campos modificables de la tabla con los valores leídos previamente. Si el registro no se encuentra significa que otro usuario lo ha modificado, y entonces se obliga al usuario a repetir la operación.

Esta aproximación también puede usarse en otros casos, aunque cuando la transacción incluye muchas operaciones, puede ser muy complejo. Para estas situaciones, también se puede usar la opción de bloquear los registros, como se explica a continuación.

5.1.3.2.1. Bloqueos

Las operaciones DML update y delete bloquean implícitamente los registros afectados. Sin embargo hay ocasiones en las que primero leemos información de la BD, y en función de ella actualizamos otra información. Ejemplos de estos casos son al incrementar un saldo, obtener un numero secuencial, ... Para estas situaciones nos interesa bloquear los registros que leemos, y así nos aseguramos que no van a cambiar mientras hacemos la actualización.

En las conexiones con autocommit, hay que tener la precaución de empezar una transacción antes de hacer los bloqueos (o asegurarnos que estamos en una transacción ya empezada). Los bloqueos se liberan cuando la transacción finaliza. También hay que recordar que al finalizar cada petición al servidor, se cierran las conexiones con las bases de datos, y por tanto también se liberan todos los bloqueos. Eso significa que por ejemplo, no podemos mantener bloqueado un registro mientras el usuario lo edita.

Los bloqueos se hacen sin espera (si el registro está bloqueado por otro usuario se produce un error), con el fin de no colapsar el servidor web. En el caso de mysql no se dispone de la opción de no esperar, aunque la espera se corta tras un timeout. Este timeout por defecto es de 50 segundos, por lo que conviene cambiar el parámetro innodb_lock_wait_timeout a un valor de menos de 5 segundos.

Para hacer estos bloqueos, tenemos en IgepConexion el método consultarForUpdate que funciona exactamente como consultar pero bloqueando los registros afectados. Si algún registro ya esta bloqueado por otro usuario, se produce una excepción que habrá que capturar. A continuación tenemos un ejemplo de su utilización:

$con->empezarTransaccion();
try {
   $res = $con->consultarForUpdate('select * from tinv_donantes where orden=5');
   if ($res == -1) {
      $this->showMensaje('APL-x'); // algun problema con la consulta
      $con->acabarTransaccion(1);
      return -1;
   }
} catch (gvHidraLockException $e) {
   $this->showMensaje('APL-y'); // algun registro bloqueado
   $con->acabarTransaccion(1);
   return -1;
}
// actualizaciones varias relacionadas con el registro que tenemos bloqueado
//...
$con->acabarTransaccion(0);

También podemos hacer lo mismo usando el método preparedQueryForUpdate, que funciona similar pero usando sentencias preparadas:

$con->empezarTransaccion();
try {
   $res = $con->preparedQueryForUpdate('select * from tinv_donantes where orden=?', array(5,));
} catch (gvHidraLockException $e) {
   $this->showMensaje('APL-x'); // algun registro bloqueado
   $con->acabarTransaccion(1);
   return -1;
} catch (gvHidraSQLException $e) {
   $this->showMensaje('APL-y'); // algun problema con la consulta; con $e->getSqlerror() obtenemos error PEAR
   $con->acabarTransaccion(1);
   return -1;
}
// actualizaciones varias relacionadas con el registro que tenemos bloqueado
//...
$con->acabarTransaccion(0);

En el capítulo 7 se explican con más detalle las excepciones que se pueden utilizar.

En la consulta usada para bloquear conviene limitar al máximo el numero de registros obtenidos a los estrictamente necesarios, y no emplear joins ya que se bloquearían los registros afectados de todas las tablas. De esta manera aumentamos el nivel de concurrencia y reducimos la posibilidad de error por registros bloqueados.

5.1.4. Procedimientos almacenados

En este apartado intentaremos explicar como utilizar procedimientos almacenados en gvHIDRA

5.1.4.1. Introducción

Puede que nuestro sistema de información se haya diseñado depositando toda la lógica de la aplicación en procedimientos almacenados o que, puntualmente, tengamos la necesidad de acceder a uno de esos recursos del SGBD. Para ellos, en este apartado daremos algunos ejemplos de como hacerlo a través de las conexiones del FW.

5.1.4.2. Creación de una conexión

Las conexiones al framework están organizadas en tres capas (capa DBMS-gvHIDRA, capa MDB2 y driver nativo PHP). Las llamadas a procedimientos almacenados se pueden realizar desde cualquier a de las capas, pero ganamos en versatilidad a medida que nos acercamos al driver nativo.

La capa DBMS-gvHIDRA actualmente sólo te permite trabajar mediante el método operar, lo cual, excepto para casos muy puntuales, no parece cómodo. La capa MDB2 tiene métodos como el executeStoredProc especialmente para estos casos. De todos modos, nuestra recomendación es utilizar el driver nativo ya que parece que en los dos casos anteriores no se trabaja bien con valores entrada/salida.

$conexion = new IgepConexion($dsn);
$con = $conexion->getPEARConnection()->getConnection();
$query="begin proced('1234', :retorno, :msgproc); end;";
$id_result= @OCIParse( $con, $query);
if (!id_result or OCIError($id_result))
        return;
OCIBindByName ( $id_result, ":retorno", $ret, 10 );
OCIBindByName ( $id_result, ":msgproc", $msg, 200 );
@OCIExecute ( $id_result, OCI_DEFAULT );//ATENCION A partir de PHP 5.3.2 cambiar OCI_DEFAULT por OCI_NO_AUTO_COMMIT

5.1.5. Recomendaciones en el uso de SQL

En este documento se dan algunas recomendaciones para que las sentencias SQL usadas en gvHidra sean lo más estándar posible, y minimizar el impacto de un posible cambio de gestor de base de datos (SGBD). Los SGBD considerados son PostgreSQL, Oracle y Mysql.

5.1.5.1. Alias en las columnas

Se recomienda poner siempre alias en los campos de una consulta.

Si no usamos los alias, en oracle los nombres de las columnas siempre saldrán en mayúsculas, con lo que no funcionará si cambiamos o otro SGBD. En PEAR::MDB2 hay una opción de compatibilidad que transforma todo a minúscula, pero como no permite tener identificadores mixtos (del tipo 'NombreProveedor'), se ha optado por no habilitarla. Para que el alias tenga efecto hay que ponerlo con comillas dobles:

select dpro as "nombre"
from provincias

5.1.5.2. Comprobación de nulos

No usar NVL ni DECODE en oracle ya que no existen en Postgresql ni en Mysql. Mejor usar el case que funciona en todos y es estándar SQL (el coalesce es más parecido al nvl y también es estándar, aunque no funciona con oracle 8).

SELECT CASE WHEN cpro is null THEN '-' ELSE cpro END
FROM tcom_usuarios

5.1.5.3. Concatenaciones

Las concatenaciones se hacen con la función 'concat(str1,str2)' (estándar, en Mysql y oracle ya existe, en postgresql se crea (con el script a continuación); el operador '||' funcionaba en oracle y postgres aunque no se recomienda).

CREATE OR REPLACE FUNCTION concat(text, text)
  RETURNS text AS
$BODY$
  BEGIN
    RETURN coalesce($1,'') || coalesce($2,'');
  END;
$BODY$
  LANGUAGE 'plpgsql' VOLATILE;

GRANT EXECUTE ON FUNCTION concat(text, text) TO public;