Doctrine

RECU-0260 (Recurso Referencia)

Descripción

Doctrine es una librería para PHP que permite trabajar con un esquema de base de datos como si fuese un conjunto de objetos, y no de tablas y registros. Doctrine está inspirado en Hibernate, que es uno de los ORM más populares y grandes que existen, y brinda una capa de abstracción de la base de datos muy completa. La característica más importante es que ofrece la posibilidad de escribir consultas de base de datos en un lenguaje propio llamado Doctrine Query Language (DQL).

Características

Doctrine es una librería muy completa y muy configurable, por lo que es difícil resumir las principales características a destacar. A continuación se va a ofrecer un breve resumen de sus características más importantes:

  • Permite la generación automática del modelo: El mapeado ORM consiste en la creación de clases que representen al modelo de negocio de la aplicación. Estas clases son relacionadas con el esquema de las bases de datos mediante la interpretación del ORM. Dada la similitud que suele existir en el diseño relacional y el de clases, Doctrine aprovecha la similitud y crea el modelo de clases a partir del modelo relacional de tablas.
  • Posibilidad de trabajar con YAML: Se puede generar el mapeo de tablas de datos y relaciones de forma manual. Para ello, Doctrine ofrece la posibilidad de utilizar YAML, que es un formato de serialización de datos legible muy usado para este fin.
  • Simplificación de la herencia: Prácticamente todo nuestro modelo heredará de estas dos clases Doctrine_Record y Doctrine_Table. Doctrine_Record representa una entidad con sus propiedades (columnas) y nos facilita métodos para insertar, actualizar o eliminar registros, entre otros. La clase Doctrine_Table representa el esquema de una tabla. A través de esta clase se puede, por ejemplo, obtener información sobre las columnas o buscar registros específicos.
  • Facilidad de búsqueda: Doctrine permite realizar búsquedas de registros basadas en cualquier campo de una tabla. Existen métodos como findByX() que permiten realizar filtros de este tipo.
  • Relaciones entre Entidades: En Doctrine, una vez que hemos definido nuestro modelo (o se ha creado de forma automática) con las tablas y sus relaciones, resulta fácil acceder y moverse por entidades relacionadas.
  • Posee un lenguaje propio: Doctrine tiene su propio lenguaje DQL (Doctrine Query Language) para manejar las interacciones con la base de datos. Es importante considerar el uso de DQL para obtener la información a cargar en lugar de usar la “forma automática” de Doctrine para mejorar el rendimiento. Presenta las siguientes características:
    • Está diseñado para extraer objetos, no filas, que es lo que nos interesa.
    • Entiende las relaciones, por lo que no es necesario escribir los joins a mano.
    • Portable con diferentes bases de datos.

Transacciones y concurrencia

Manejo de Transacciones

La demarcación de transacciones es la tarea de definir sus límites de transacción. Usar una demarcación adecuada es muy importante porque si no se hace correctamente puede afectar negativamente el rendimiento de su aplicación. Muchas bases de datos y las capas de base de datos de abstracción, como POD, pueden operar de forma predeterminada en modo auto-commit, lo que significa que todas y cada una de las instrucciones SQL se envuelven en una operación pequeña.

En su mayor parte, Doctrine se encarga de la demarcación correcta de la transacción por nosotros. Todas las operaciones de escritura (INSERT / UPDATE / DELETE) se ponen en cola hasta que el método flush() del EntityManager se invoca,  que es quien envuelve todos estos cambios en una sola transacción. Sin embargo, Doctrine también nos permite hacernos cargo de la demarcación y el control de transacciones por nosotros mismos.

El primer enfoque consiste en utilizar la transacción implícita de gestión previstas por el EntityManager ORM. Dado el siguiente fragmento de código, sin ningún tipo de demarcación de transacciones explícitas:

<?php

// $em instanceof EntityManager
$user = new User;
$user->setName('George');
$em->persist($user);
$em->flush();

?>

Dado que no existe ninguna demarcación de transacciones personalizadas en el código anterior, el método flush() del EntityManager comenzará a ejecutar una transacción. Este comportamiento es posible gracias a la agregación de las operaciones DML por Doctrine y es suficiente si todas las operaciones de manipulación de datos que forman parte de una unidad de trabajo pasa a través del modelo de dominio y, por lo tanto, del ORM

El segundo enfoque es controlar los límites de la transacción de forma directa mediante el uso de la API específica para ello de Doctrine. El código es el siguiente:

<?php

// $em instanceof EntityManager
$em->getConnection()->beginTransaction(); // suspend auto-commit
try {
    //... do some work
    $user = new User;
    $user->setName('George');
    $em->persist($user);
    $em->flush();
    $em->getConnection()->commit();
} catch (Exception $e) {
    $em->getConnection()->rollback();
    $em->close();
    throw $e;
}
>

La demarcación de transacción explícita es necesaria cuando se desea incluir las operaciones de DBAL personalizadas en una unidad de trabajo o cuando se desea hacer uso de algunos métodos de la API de EntityManager que requieren una transacción activa. Tales métodos lanzarán una TransactionRequiredException para informarlo acerca de ese requisito.

Una alternativa más conveniente para la demarcación de transacción explícita es el uso de abstracciones de control previsto con el método transactional($ func). Cuando se utilicen estas abstracciones de control se asegurará que nunca se olvida de deshacer la operación o cerrar el EntityManager, además de la reducción del código evidente. Un ejemplo que es funcionalmente equivalente al código anteriormente mostrado es el siguiente:

<?php

// $em instanceof EntityManager
$em->transactional(function($em) {
    //... do some work
    $user = new User;
    $user->setName('George');
    $em->persist($user);
});
>

Manejo de Concurrencia

Las transacciones de bases de datos están muy bien para el control de concurrencia en una única solicitud. Sin embargo, la operación de base de datos no debe extenderse a lo largo peticiones. Por lo tanto, una "transacción comercial" de larga duración, que abarca varias solicitudes, deberá participar en varias operaciones de base de datos. Por lo tanto, las transacciones de base de datos sólo pueden controlar que no haya concurrencia durante una misma operación comercial. El control de concurrencia se convierte en la responsabilidad parcial de la propia aplicación.

Doctrine ha integrado soporte para bloqueo optimista automático por medio de un campo de versión. En este enfoque, cualquier entidad que deba estar protegida contra modificaciones concurrentes durante las transacciones de larga duración obtienen un campo de versión que es un número simple (tipo de asignación: entero) o una marca de tiempo (tipo de asignación: fecha y hora). Cuando los cambios en esa entidad se conservan en el final de una conversación larga que ejecuta la versión de la entidad, se compara con la versión de la base de datos y si no coinciden se produce un OptimisticLockException, lo que indica que la entidad ha sido modificada por alguien. Se puede designar un campo de versión en una entidad de la siguiente manera. En este ejemplo vamos a utilizar un número entero:

<?php

class User
{
    // ...
    /** @Version @Column(type="integer") */
    private $version;
    // ...
}

Doctrine es compatible con el bloqueo pesimista en el nivel de base de datos. No se intenta aplicar el bloqueo pesimista dentro de Doctrine, mediante comandos ANSI-SQL se realizan los bloqueos a nivel de fila. Cada entidad puede ser parte de un bloqueo pesimista, no hay metadatos especiales necesarios para utilizar esta característica. Doctrine lanzará una excepción si intenta adquirir un bloqueo pesimista y la transacción no se está ejecutando. Doctrine actualmente soporta dos modos de bloqueo pesimista:

  • Escritura pesimista (DoctrinaDBALLockMode::PESSIMISTIC_WRITE), bloquea la base de datos subyacente de filas simultáneas de lectura y escritura de Operaciones.
  • Lectura pesimista (DoctrinaDBALLockMode::PESSIMISTIC_READ), bloquea solicitudes simultáneas que intentan actualizar o bloquear filas en modo de escritura.

Caché

Doctrine proporciona controladores de caché en el conjunto común de algunas de las implementaciones de almacenamiento en caché más populares, tales como APC, Memcache y XCache. También proporciona un controlador ArrayCache que almacena los datos en un array de PHP. Obviamente, la caché no vive entre las peticiones, pero esto es útil para realizar pruebas en un entorno de desarrollo.

Los controladores de caché deben seguir una sencilla interfaz que se define en DoctrineCommon Cache. Todos los proveedores de caché deben extender de la clase DoctrinaCommon Cache AbstractCache que implementa la interfaz antes mencionada. Los métodos son los siguientes:

  • fetch($id) - Realiza búsquedas dentro de la caché.
  • contains($id) - Comprueba que la entrada existe en la caché
  • save($id, $data, $lifeTime = false) Introduce datos en la caché
  • delete($id) - Borra una entrada de la caché

Doctrine ofrece diversos tipos de caché:

  • Caché de consultas: Es muy recomendable que, en un entorno de producción, se realice la transformación de caché de una consulta DQL a su homólogo SQL. No tiene sentido hacer esto al analizar varias veces, ya que no cambiará a menos que modifique la consulta DQL.
  • Caché de resultados: La caché de resultados puede ser usada para almacenar en caché los resultados de las consultas de manera que no es necesario consultar la base de datos o refrescar los datos de nuevo después de la primera vez.
  • Caché de datos: Sus metadatos se pueden analizar a partir de unas pocas fuentes diferentes como YAML, XML, anotaciones, etc. En lugar de analizar esta información en cada solicitud, se puede utilizar una caché utilizando uno de los impulsores de la caché.

Mapeo en YAML

Doctrine permite proporcionar los metadatos ORM en forma de documentos YAML. El documento de mapeo YAML de una clase se carga a la carta la primera vez que se solicita y puede ser almacenado en la caché de metadatos. Para que funcione, esto requiere ciertas convenciones:

  • Cada entidad / superclase asignada debe tener su propio documento de mapeo dedicada YAML.
  • El nombre del documento de mapeo debe consistir en el nombre completo de la clase, donde los separadores de espacio de nombres se sustituyen por puntos (.).
  • Todos los documentos de mapeo debe tener la extensión ". Dcm.yml" para identificarlo como un archivo de asignación de Doctrine. Puede cambiar la extensión de archivo con bastante facilidad.

Se recomienda almacenar todos los documentos de asignación de YAML en una sola carpeta, pero se puede diseminar los documentos en varias carpetas si así lo desea. Con el fin de contar con un manejador YAML , YamlDriver, dónde buscar los documentos de su asignación, se necesita que como el primer argumento del constructor se asigne la ruta, como el ejemplo siguiente:

<?php

// $config instanceof Doctrine\ORM\Configuration
$driver = new YamlDriver(array('/path/to/files'));
$config->setMetadataDriverImpl($driver);

El lenguaje DQL

El lenguaje Doctrine Query Lenguage es una consulta de objetos que es muy similar al lenguaje de consultas de Hibernate (HQL) o el Java Persistence Query Language (JPQL). En esencia, DQL proporciona potentes capacidades de consulta sobre su modelo de objetos. Imagine todos los objetos dispersos en algún almacenamiento (como un objeto de base). Al escribir consultas DQL, piense que la consulta recoge un cierto subconjunto de los objetos del almacenamiento.

DQL como un lenguaje de consulta tiene instrucciones del tipo SELECT, UPDATE y DELETE que se asignan a sus correspondientes sentencias SQL. INSERT no se permite en DQL, porque las entidades y sus relaciones tienen que ser introducidos en el contexto de persistencia a través del metodo persistence () del EntityManager para garantizar la coherencia de su modelo de objetos.

DQL proporciona sentencias SELECT que son una manera muy poderosa de recuperar partes de su modelo de dominio que no son accesibles a través de asociaciones. Además, permiten recuperar las entidades y sus asociaciones en una sola instrucción SELECT de SQL que puede marcar una gran diferencia en el rendimiento en contraste con el empleo de varias consultas.

Las instrucciones UPDATE y DELETE ofrecen una manera de ejecutar cambios masivos en las entidades de su modelo de dominio. Esto es a menudo necesario cuando no se puede cargar todas las entidades afectadas de una actualización en masa en la memoria.

Anotaciones

Doctrine permite el uso de anotaciones, como las introducidas por Java para facilitar el establecimiento de las relaciones para realizar el mapeado ORM y facilitar el manejo de la persistencia. A continuación se presentan las más significativas

  • @Column
  • @Entity
  • @GeneratedValue
  • @Id
  • @JoinColumn
  • @JoinTable
  • @ManyToOne
  • @ManyToMany
  • @OneToOne
  • @OneToMany
  • @OrderBy
  • @Table
  • @Version

Ejemplos

A continuación se va a realizar un ejemplo básico para facilitar la comprensión de Doctrine, consistente en una implementación muy sencilla que se basa en un listado de comentarios escritos por usuarios. Cada vez que se desee insertar un nuevo comentario, se deberá rellenar un formulario con: nombre, e-mail y texto, siendo los dos últimos obligatorios. Si el usuario no deja su nombre, se mostrará como “Desconocido”.

Crear el esquema de base de datos

El ejemplo es tan sencillo que solamente tendrá dos tablas relacionadas entre sí: users y users_comments.

CREATE SCHEMA  ontuts_doctrine CHARSET UTF8 COLLATE utf8_general_ci;  
use ontuts_doctrine; 
CREATE TABLE users( 
     id INT(11) PRIMARY  KEY auto_increment, 
     name VARCHAR(30), 
     email VARCHAR(60) 
)ENGINE=INNODB; 
CREATE TABLE  users_comments( 
     id INT(11) PRIMARY KEY auto_increment, 
     id_user INT(11)  NOT NULL, 
     text VARCHAR(255), 
CONSTRAINT fk_users_comments_users  FOREIGN KEY (id_user) REFERENCES users(id) 
)ENGINE=INNODB;

Crear modelo

Se puede hacer mediante el uso de la automatización o mediante el formato manual (YAML). En este ejemplo, realizaremos la generación automática para explicar cómo funciona. Vamos a crear el script que se encargará de generar los ficheros, create_orm_model.php

<?php  
require_once(dirname(__FILE__)  . '/lib/Doctrine.php'); 
spl_autoload_register(array('Doctrine',  'autoload')); 
$conn  =  Doctrine_Manager::connection('mysql://root:password@localhost/ontuts_doctrine',  'doctrine'); 
Doctrine_Core::generateModelsFromDb('models',  array('doctrine'), array('generateTableClasses' => true));
>

Una vez se ha ejecutado con éxito, vamos a la carpeta models y vemos que se han generado varios ficheros:

  • Users.php
  • UsersTable.php
  • UserComments.php
  • UserCommentsTable.php
  • generated/BaseUsers.php
  • generated/BaseUsersComments.php

Si nos fijamos, se han creado las clases BaseUsers.php y BaseUsersComments.php, que se extienden de Doctrine_Record y Doctrine_Table,  que representan las clases base y que no deberían ser modificadas. Cualquier método o propiedad que desees añadir, debes hacerlo sobre Users.php o UsersComments.php.

Extender el modelo

Analizando la aplicación llegamos a la conclusión de que la clase Users necesita dos nuevos métodos:

  • getName: que devuelva el nombre del usuario o “Desconocido” si no ha rellenado esa información.
  • addComment: que inserte un nuevo comentario asignado a dicho usuario.
<?php   
class Users extends  BaseUsers 
 { 
     public function addComment($text){ 
         $comment = new UsersComments(); 
         $comment->id_user = $this->id; 
         $comment->text = $text; 
         $comment->save(); 
     } 
     public function  getName(){ 
         if(emptyempty($this->name) || is_null($this->name)){ 
             return  "Desconocido"; 
         }else{ 
             return $this->name; 
         } 
     } 
 }

Crear lógica

A continuación creamos el fichero index.php que contendrá la lógica principal de la aplicación. Nos queda algo así: index.php

<?php  
 //Carga Doctrine 
 require_once(dirname(__FILE__)  . '/lib/Doctrine.php'); 
 spl_autoload_register(array('Doctrine',  'autoload')); 
 $conn =  Doctrine_Manager::connection('mysql://root@localhost/ontuts_doctrine',  'doctrine'); 
 $conn->setCharset('utf8'); 
 Doctrine_Core::loadModels('models'); 
  
 //Variable en la  plantilla html 
 $tpl = array("comments"=> array(), "error"=>false); 
  
 //Comprueba si se ha  enviado el formulario 
 if(!emptyempty($_POST) &&  isset($_POST['create_comment'])){ 
     $email = filter_input(INPUT_POST,  "email", FILTER_SANITIZE_STRING); 
     $name = filter_input(INPUT_POST,  "name", FILTER_SANITIZE_STRING); 
     $text = filter_input(INPUT_POST,  "text", FILTER_SANITIZE_STRING); 
     //Comprueba que se hayan rellenado  los campos obligatorios 
     if(!emptyempty($email) &&  !is_null($email) && 
     !emptyempty($text) &&  !is_null($text)){ 
         $userTable = Doctrine_Core::getTable('Users'); 
         $users =  $userTable->findByEmail($email); 
         $user = null; 
         //Si el  usuario no existe, lo crea 
         if($users->count()==0){ 
             $user =  new Users(); 
             $user->name = $name; 
             $user->email = $email; 
             $user->save(); 
         }else{ 
             $user =  $users[0]; 
         } 
         //Inserta el comentario 
         $user->addComment($text); 
     }else{ 
         //Si no se se  han rellenado todos los valores obligatorios 
         //mostrará un  error 
         $tpl['error'] = true; 
     } 
 } 
 //Carga los comentarios 
 $commentsTable =  Doctrine_Core::getTable('UsersComments'); 
 $tpl['comments'] =  $commentsTable->findAll(); 
 //Envia la información 
 require_once('template.phtml');

Enlaces externos

Contenidos relacionados