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 te da 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 mas 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 de 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 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 instrucción SQL se envuelve en una operación pequeña. Sin que se encuentre delimitado transacción explícita de su lado, los resultados de forma rápida en un rendimiento inferior porque las transacciones no son baratas.

En su mayor parte, Doctrine ya se encarga de la demarcación de transacción correcta 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 envuelve todos estos cambios en una sola transacción. Sin embargo, la Doctrine también permite a hacerse 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 forma parte de una unidad de trabajo que pasa a través del modelo de dominio y por lo tanto del ORM

El segundo enfoque, es controlar los limites 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 en con el método transactional($ func). Cuando se utilicen estas abstracciones de control se asegura 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 "transacciones comercial" de larga duración, que abarca varias solicitudes, deberán participar varias operaciones de base de datos. Por lo tanto, las transacciones de base de datos solo puede controlar ya que no haya concurrencia durante la misma una 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 protegido 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 larga conversación y 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 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.

Cache

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 cache no vive entre las peticiones, pero esto es útil para realizar pruebas en un entorno de desarrollo.

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

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

Doctrine ofrece diversos tipos de cache

  • Cache 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.
  • Cache de resultados La cache 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.
  • Cache de datos Su metadatos se puede 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 cache utilizando uno de los impulsores de la cache.

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, piensa que ka 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 empleando 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, consiste una implementación muy sencilla de Doctrine 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 como 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 te fijas, se han creado las clases que se extienden de Doctrine_Record y Doctrine_Table que ya BaseUsers.php y BaseUsersComments.php representan clases base y que no deberías modificar. 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