Archivo

Archivo para la Categoría "Doctrine"

Un vistazo al QueryBuilder de Doctrine 2

2 abril, 2011 Dejar un comentario

Acabado el capítulo sobre DQL pasamos a ver el capítulo que trata el QueryBuilder, un objeto que proporciona una API fluida para construir consultas DQL por pasos. Para quién no sepáis qué es una API fluida podéis consultar la Wikipedia

De la misma forma que instanciamos un objeto Query podemos instanciar un objeto QueryBuilder:

<?php
$qb = $em->createQueryBuilder();

Podemos inspeccionar que tipo de objeto es el QueryBuilder:

<?php
echo $qb->getType(); // imprime: 0

Hay tres tipos:

  • QueryBuilder::SELECT, retorna el valor 0
  • QueryBuilder::DELETE, retorna el valor 1
  • QueryBuilder::UPDATE, retorna el valor 2

También podemos obtener el EntityManager asociado, el string DQL definido en el QueryBuilder, y el objeto Query con el DQL procesado:

<?php
$em = $qb->getEntityManager();
 
$dql = $qb->getDql();
 
$q = $qb->getQuery();

Internamente, QueryBuilder trabaja con una cache DQL para incrementar el rendimiento. Cualquier cambio que afecte al DQL generado modifica el estado del QueryBuilder a STATE_DIRTY. Hay dos estados:

  • QueryBuilder::STATE_CLEAN, que significa que no han habido cambios
  • QueryBuilder::STATE_DIRTY, que significa que la consulta DQL debe ser procesada

El método add de QueryBuilder es el responsable de construir pieza a pieza la consulta DQL. Toma tres parámetros:

  1. $dqlPartName: El nombre de la parte a la que hace referencia $dqlPart. Puede ser select, from, where, groupBy, having, orderBy.
  2. $dqlPart: El valor que acompaña a $dqlPartName. Puede ser un string o una instancia de Doctrine\ORM\Query\Expr\*
  3. $append: Flag opcional (por defecto FALSE) que establece si $dqlPart debe sobreescribir el valor si ya se había aplicado antes.
<?php
$qb->add('select', 'u')
   ->add('from', 'User u')
   ->add('where', 'u.id = ?1')
   ->add('orderBy', 'u.name ASC');

Este QueryBuilder crea la consulta DQL siguiente:

"SELECT u FROM User u WHERE u.id = ?1 ORDER BY u.name ASC"

Doctrine 2 nos permite establecer parámetros en las consultas para ayudarnos a construir consultas dinámicas:

<?php
 
$qb->add('select', 'u')
   ->add('from', 'User u')
   ->add('where', 'u.id = ?1')
   ->add('orderBy', 'u.name ASC');
   ->setParameter(1, 100); // Sustituye ?1 por 100
 
$qb->add('select', 'u')
   ->add('from', 'User u')
   ->add('where', 'u.id = :identifier')
   ->add('orderBy', 'u.name ASC');
   ->setParameter('identifier', 100); //Sustituye :identifier por 100

Si tenemos que establecer más de un parámetro podemos utilizar setParameters() en lugar de setParameter():

<?php 
$qb->setParameters(array(1 => 'value for ?1', 2 => 'value for ?2'));

Para limitar un conjunto de resultados podemos utilizar los métodos setFirstResult() y setMaxResults():

<?php
$offset = (int)$_GET['offset'];
$limit = (int)$_GET['limit'];
 
$qb->add('select', 'u')
   ->add('from', 'User u')
   ->add('orderBy', 'u.name ASC')
   ->setFirstResult( $offset )
   ->setMaxResults( $limit );

El QueryBuilder solamente es un objeto que nos construye la consulta DQL, por lo que si queremos ejecutar la consulta primero tenemos que obtenerla con el método getQuery:

<?php
$query = $qb->getQuery();
 
// Establecer opciones adicionales
$query->setQueryHint('foo', 'bar');
$query->useResultCache('my_cache_id');
 
// Ejecutar la consulta
$result = $query->getResult();
$single = $query->getSingleResult();
$array = $query->getArrayResult();
$scalar = $query->getScalarResult();
$singleScalar = $query->getSingleScalarResult();

Si invocamos el método add con un string, internamente se evalúa a una expresión. Si no queremos utilizar los atajos podemos crear las expresiones adecuadas para cada opción:

<?php
$qb->add('select', new Expr\Select(array('u')))
   ->add('from', new Expr\From('User', 'u'))
   ->add('where', new Expr\Comparison('u.id', '=', '?1'))
   ->add('orderBy', new Expr\OrderBy('u.name', 'ASC'));

Desde luego, esta manera es bastante engorrosa de crear consultas DQL, por lo que la clase Expr provee de un conjunto de métodos que ayudan a crear este tipo de expresiones:

$qb->add('select', $qb->expr()->select('u'))
   ->add('from', $qb->expr()->from('User', 'u'))
   ->add('where', $qb->expr()->orx(
       $qb->expr()->eq('u.id', '?1'),
       $qb->expr()->like('u.nickname', '?2')
   ))
   ->add('orderBy', $qb->expr()->orderBy('u.surname', 'ASC'));

NOTA del traductor: No creo que utilizar esta forma de construir consultas a través de expresiones de este tipo aumente mucho el rendimiento de la aplicación, debido a que si utilizamos una cache para cachear las consultas SQL procesadas se procesarán muy pocas veces. Por mi parte, prefiero legibilidad antes que escirbir tal cantidad de código para escribir una simple consulta DQL. Supongo que habrá veces que no quede más remedio que utilizar ciertas expresiones pero si puedo evitar esta forma de crear DQLs mucho mejor.

Os adjunto los métodos con los que cuenta la clase Expr para crear expresiones. No voy a comentar tal cantidad de métodos, dado que la mayoría son autodescriptivos.

<?php
class Expr
{
    /** Base objects **/
 
    // Example usage - $qb->expr()->select('u')
    public function select($select = null); // Returns Expr\Select instance
 
    // Example - $qb->expr()->from('User', 'u')
    public function from($from, $alias); // Returns Expr\From instance
 
    // Example - $qb->expr()->leftJoin('u.Phonenumbers', 'p', Expr\Join::ON, 'p.user_id = u.id AND p.country_code = 55');
    // Example - $qb->expr()->leftJoin('u.Phonenumbers', 'p', 'ON', $qb->expr()->andx($qb->expr()->eq('p.user_id', 'u.id'), $qb->expr()->eq('p.country_code', '55'));
    public function leftJoin($join, $alias, $conditionType = null, $condition = null); // Returns Expr\Join instance
 
    // Example - $qb->expr()->innerJoin('u.Group', 'g', Expr\Join::WITH, 'g.manager_level = 100');
    // Example - $qb->expr()->innerJoin('u.Group', 'g', 'WITH', $qb->expr()->eq('g.manager_level', '100'));
    public function innerJoin($join, $alias, $conditionType = null, $condition = null); // Returns Expr\Join instance
 
    // Example - $qb->expr()->orderBy('u.surname', 'ASC')->add('u.firstname', 'ASC')->...
    public function orderBy($sort = null, $order = null); // Returns Expr\OrderBy instance
 
    // Example - $qb->expr()->groupBy()->add('u.id')->...
    public function groupBy($groupBy = null); // Returns Expr\GroupBy instance
 
 
    /** Conditional objects **/
 
    // Example - $qb->expr()->andx($cond1 [, $condN])->add(...)->...
    public function andx($x = null); // Returns Expr\Andx instance
 
    // Example - $qb->expr()->orx($cond1 [, $condN])->add(...)->...
    public function orx($x = null); // Returns Expr\Orx instance
 
 
    /** Comparison objects **/
 
    // Example - $qb->expr()->eq('u.id', '?1') => u.id = ?1
    public function eq($x, $y); // Returns Expr\Comparison instance
 
    // Example - $qb->expr()->neq('u.id', '?1') => u.id <> ?1
    public function neq($x, $y); // Returns Expr\Comparison instance
 
    // Example - $qb->expr()->lt('u.id', '?1') => u.id < ?1
    public function lt($x, $y); // Returns Expr\Comparison instance
 
    // Example - $qb->expr()->lte('u.id', '?1') => u.id <= ?1
    public function lte($x, $y); // Returns Expr\Comparison instance
 
    // Example - $qb->expr()->gt('u.id', '?1') => u.id > ?1
    public function gt($x, $y); // Returns Expr\Comparison instance
 
    // Example - $qb->expr()->gte('u.id', '?1') => u.id >= ?1
    public function gte($x, $y); // Returns Expr\Comparison instance
 
 
    /** Arithmetic objects **/
 
    // Example - $qb->expr()->prod('u.id', '2') => u.id * 2
    public function prod($x, $y); // Returns Expr\Math instance
 
    // Example - $qb->expr()->diff('u.id', '2') => u.id - 2
    public function diff($x, $y); // Returns Expr\Math instance
 
    // Example - $qb->expr()->sum('u.id', '2') => u.id + 2
    public function sum($x, $y); // Returns Expr\Math instance
 
    // Example - $qb->expr()->quot('u.id', '2') => u.id / 2
    public function quot($x, $y); // Returns Expr\Math instance
 
 
    /** Pseudo-function objects **/
 
    // Example - $qb->expr()->exists($qb2->getDql())
    public function exists($subquery); // Returns Expr\Func instance
 
    // Example - $qb->expr()->all($qb2->getDql())
    public function all($subquery); // Returns Expr\Func instance
 
    // Example - $qb->expr()->some($qb2->getDql())
    public function some($subquery); // Returns Expr\Func instance
 
    // Example - $qb->expr()->any($qb2->getDql())
    public function any($subquery); // Returns Expr\Func instance
 
    // Example - $qb->expr()->not($qb->expr()->eq('u.id', '?1'))
    public function not($restriction); // Returns Expr\Func instance
 
    // Example - $qb->expr()->in('u.id', array(1, 2, 3))
    // Make sure that you do NOT use something similar to $qb->expr()->in('value', array('stringvalue')) as this will cause Doctrine to throw an Exception.
    // Instead, use $qb->expr()->in('value', array('?1')) and bind your parameter to ?1 (see section above)
    public function in($x, $y); // Returns Expr\Func instance
 
    // Example - $qb->expr()->notIn('u.id', '2')
    public function notIn($x, $y); // Returns Expr\Func instance
 
    // Example - $qb->expr()->like('u.firstname', $qb->expr()->literal('Gui%'))
    public function like($x, $y); // Returns Expr\Comparison instance
 
    // Example - $qb->expr()->between('u.id', '1', '10')
    public function between($val, $x, $y); // Returns Expr\Func
 
 
    /** Function objects **/
 
    // Example - $qb->expr()->trim('u.firstname')
    public function trim($x); // Returns Expr\Func
 
    // Example - $qb->expr()->concat('u.firstname', $qb->expr()->concat(' ', 'u.lastname'))
    public function concat($x, $y); // Returns Expr\Func
 
    // Example - $qb->expr()->substr('u.firstname', 0, 1)
    public function substr($x, $from, $len); // Returns Expr\Func
 
    // Example - $qb->expr()->lower('u.firstname')
    public function lower($x); // Returns Expr\Func
 
    // Example - $qb->expr()->upper('u.firstname')
    public function upper($x); // Returns Expr\Func
 
    // Example - $qb->expr()->length('u.firstname')
    public function length($x); // Returns Expr\Func
 
    // Example - $qb->expr()->avg('u.age')
    public function avg($x); // Returns Expr\Func
 
    // Example - $qb->expr()->max('u.age')
    public function max($x); // Returns Expr\Func
 
    // Example - $qb->expr()->min('u.age')
    public function min($x); // Returns Expr\Func
 
    // Example - $qb->expr()->abs('u.currentBalance')
    public function abs($x); // Returns Expr\Func
 
    // Example - $qb->expr()->sqrt('u.currentBalance')
    public function sqrt($x); // Returns Expr\Func
 
    // Example - $qb->expr()->count('u.firstname')
    public function count($x); // Returns Expr\Func
 
    // Example - $qb->expr()->countDistinct('u.surname')
    public function countDistinct($x); // Returns Expr\Func
}

Como hemos visto, la forma anterior es una forma un poco engorrosa de crear consultas DQL. Para simplificar más las cosas la clase QueryBuilder provee de un conjunto de métodos de ayuda que son atajos a los vistos anteriormente para hacer la vida del programador mucho más fácil y llevadera.

<?php
$qb->select('u')
   ->from('User', 'u')
   ->where('u.id = ?1')
   ->orderBy('u.name ASC');

Os adjunto la lista de métodos disponibles:

<?php
class QueryBuilder
{
    // Example - $qb->select('u')
    // Example - $qb->select(array('u', 'p'))
    // Example - $qb->select($qb->expr()->select('u', 'p'))
    public function select($select = null);
 
    // Example - $qb->delete('User', 'u')
    public function delete($delete = null, $alias = null);
 
    // Example - $qb->update('Group', 'g')
    public function update($update = null, $alias = null);
 
    // Example - $qb->set('u.firstName', $qb->expr()->literal('Arnold'))
    // Example - $qb->set('u.numChilds', 'u.numChilds + ?1')
    // Example - $qb->set('u.numChilds', $qb->expr()->sum('u.numChilds', '?1'))
    public function set($key, $value);
 
    // Example - $qb->from('Phonenumber', 'p')
    public function from($from, $alias = null);
 
    // Example - $qb->innerJoin('u.Group', 'g', Expr\Join::ON, $qb->expr()->and($qb->expr()->eq('u.group_id', 'g.id'), 'g.name = ?1'))
    // Example - $qb->innerJoin('u.Group', 'g', 'ON', 'u.group_id = g.id AND g.name = ?1')
    public function innerJoin($join, $alias = null, $conditionType = null, $condition = null);
 
    // Example - $qb->leftJoin('u.Phonenumbers', 'p', Expr\Join::WITH, $qb->expr()->eq('p.area_code', 55))
    // Example - $qb->leftJoin('u.Phonenumbers', 'p', 'WITH', 'p.area_code = 55')
    public function leftJoin($join, $alias = null, $conditionType = null, $condition = null);
 
    // NOTE: ->where() overrides all previously set conditions
    //
    // Example - $qb->where('u.firstName = ?1', $qb->expr()->eq('u.surname', '?2'))
    // Example - $qb->where($qb->expr()->andx($qb->expr()->eq('u.firstName', '?1'), $qb->expr()->eq('u.surname', '?2')))
    // Example - $qb->where('u.firstName = ?1 AND u.surname = ?2')
    public function where($where);
 
    // Example - $qb->andWhere($qb->expr()->orx($qb->expr()->lte('u.age', 40), 'u.numChild = 0'))
    public function andWhere($where);
 
    // Example - $qb->orWhere($qb->expr()->between('u.id', 1, 10));
    public function orWhere($where);
 
    // NOTE: -> groupBy() overrides all previously set grouping conditions
    //
    // Example - $qb->groupBy('u.id')
    public function groupBy($groupBy);
 
    // Example - $qb->addGroupBy('g.name')
    public function addGroupBy($groupBy);
 
    // NOTE: -> having() overrides all previously set having conditions
    //
    // Example - $qb->having('u.salary >= ?1')
    // Example - $qb->having($qb->expr()->gte('u.salary', '?1'))
    public function having($having);
 
    // Example - $qb->andHaving($qb->expr()->gt($qb->expr()->count('u.numChild'), 0))
    public function andHaving($having);
 
    // Example - $qb->orHaving($qb->expr()->lte('g.managerLevel', '100'))
    public function orHaving($having);
 
    // NOTE: -> orderBy() overrides all previously set ordering conditions
    //
    // Example - $qb->orderBy('u.surname', 'DESC')
    public function orderBy($sort, $order = null);
 
    // Example - $qb->addOrderBy('u.firstName')
    public function addOrderBy($sort, $order = null); // Default $order = 'ASC'
}

Como podéis ver tenemos métodos de ayuda para realizar todo tipos de consultas en DQL.

Categorías:Bases de datos, Doctrine

Un vistazo a la clase Query de Doctrine 2

26 marzo, 2011 11 comentarios

Una instancia de Doctrine\ORM\Query representa una consulta SQL. Podemos crear una instancia Query invocando el método createQuery($sql) del EntityManager, pasándole como parámetro la consulta en formato string. Otra alternativa es crear una instancia Query vacía e invocar el método setDql($dql) de Query.

<?php
// example1: pasando un string DQL
$q = $em->createQuery('select u from MyProject\Model\User u');

// example2: usando setDql
$q = $em->createQuery();
$q->setDql('select u from MyProject\Model\User u');

Tipos de formatos del resultado

El formato en el que los resultados de una consulta DQL SELECT son devueltos está influenciado por el modo en el que se realiza el proceso de hydrating.

  • getResult() Devuelve una colección de objetos. El resultado es una colección de objetos o un array de resultados.
  • getSingleResult() Devuelve un único objeto. Si el resultado contiene más de un objeto se lanza una excepción.
  • getArrayResult() Devuelve los resultados como un grafo array (un array anidado).
  • getScalarResult() Devuelve un conjunto de resultados escalares.
  • getSingleScalarResult() Devuelve un único valor escalar. Si el resultado contiene más de un valor escalar se lanza una excepción.

Podemos utilizar este conjunto de métodos o utilizar otra alternativa. Invocar el método execute de Query pasándole dos parámetros, un conjunto de parámetros y el módo en el que se realizará el proceso de hydrating.

La naturaleza de los resultados devueltos por una consulta DQL SELECT a través del método getResult() o getArrayResult() puede ser de dos tipos: puro y mixto.

Resultados puros  y mixtos

El resultado será de tipo puro cuando devuelva una colección de objetos debido a que hemos recuperado entidades completas o parciales. Si en la consulta solamente queremos recuperar algunas propiedades o otros valores escalares que no tienen ninguna relación con una entidad, el resultado se transforma en tipo mixto.

Un resultado puro tiene el siguiente formato:

array
    [0] => Object
    [1] => Object
    [2] => Object
    ...

Un resultado mixto tiene una estructura similar a la siguiente, ya que cambiará en cada caso:

array
    array
        [0] => Object
        [1] => "some scalar string"
        ['count'] => 42
        // ... más valores escalares, indexados numéricamente o por nombre
    array
        [0] => Object
        [1] => "some scalar string"
        ['count'] => 42
        // ... más valores escalares, indexados numéricamente o por nombre

Para entender mejor la diferencia entre estos dos tipos de resultados vamos a ver una consulta que produce resultados mixtos:

"SELECT u, UPPER(u.name) nameUpper FROM MyProject\Model\User u"

Esta consulta devuelve por cada coincidencia un array de dos posiciones. En la primera posición un objeto que es la entidad User y en la segunda posición un valor de tipo string que es el nombre en mayúsculas de cada entidad User.

El resultado de tipo mixto tendría la siguiente estructura:

array
    array
        [0] => User (Object)
        ['nameUpper'] => "ROMAN"
    array
        [0] => User (Object)
        ['nameUpper'] => "JONATHAN"
    ...

Para acceder a estos resultados mediante código PHP realizaríamos lo siguiente:

<?php
foreach ($results as $row) {
    echo "Name: " . $row[0]->getName();
    echo "Name UPPER: " . $row['nameUpper'];
}

Modos de hydrating

Cada modo de realizar el proceso de hydrating devuelve los resultados en diferentes tipos. Utilizamos cuatro constantes para especificar el modo:

  • Query::HYDRATE_OBJECT
  • Query::HYDRATE_ARRAY
  • Query::HYDRATE_SCALAR
  • Query::HYDRATE_SINGLE_SCALAR
Object Hydration:

Este modo devuelve los resultados como objetos:

<?php
$query = $em->createQuery('SELECT u FROM CmsUser u');
$users = $query->getResult(Query::HYDRATE_OBJECT);
Array Hydration:

Este modo devuelve los resultados como un array:

<?php
$query = $em->createQuery('SELECT u FROM CmsUser u');
$users = $query->getResult(Query::HYDRATE_ARRAY);

En este caso podemos utilizar el siguiente atajo:

<?php
$users = $query->getArrayResult();
Scalar Hydration:

Este modo devuelve los resultados como un conjunto de valores escalares:

<?php
$query = $em->createQuery('SELECT u FROM CmsUser u');
$users = $query->getResult(Query::HYDRATE_SCALAR);
echo $users[0]['u_id'];
Single Scalar Hydration:

Este modo devuelve un único resultado escalar:

<?php
$query = $em->createQuery('SELECT COUNT(a.id) FROM CmsUser u LEFT JOIN u.articles a WHERE u.username = ?1 GROUP BY u.id');
$query->setParameter(1, 'jwage');
$numArticles = $query->getResult(Query::HYDRATE_SINGLE_SCALAR);

En este caso podemos utilizar el siguiente atajo:

<?php
$numArticles = $query->getSingleScalarResult();

Parámetro posicionales

La clase AbstractQuery (padre de Query y NativeQuery) también contiene métodos para establecer y recuperar parámetros posicionales o por nombres:

  • setParameter($param, $value);
  • setParameters(array $params) ;
  • getParameter($param):
  • getParameters();

Cachear resultados y consultas

Podemos cachear los resultados de las consultas DQL pero hay que tener alguans cosas en cuenta. Vamos a ver un proceso normal de cacheo de resultados:

<?php
//Creamos la consulta
$query = $em->createQuery('SELECT u FROM MyProject\Model\User u WHERE u.id = ?1');
$query->setParameter(1, 12);

//Especificamos el driver de cache a utilizar
$query->setResultCacheDriver(new ApcCache());

//Especificamos que se cachee el resultado y que se
//guarde en la cache 3600 segundos
$query->useResultCache(true)
      ->setResultCacheLifeTime($seconds = 3600);

//Si ejecutamos así la consulta la caché no guarda los resultados.
$result = $query->getResult(); 

//Forzamos a expirar los resultados en cache, pero la caché no guarda los resultados.
$query->expireResultCache(true);
$result = $query->getResult(); 

//De esta forma si que se guardan los resultados en caché.
//Dándole un nombre al conjunto de resultados que tiene que cachear.
$query->setResultCacheId('my_query_result');
$result = $query->getResult(); 

// O podemos hacer todo junto de esta manera
$query->useResultCache(true, $seconds = 3600, 'my_query_result');
$result = $query->getResult();

Nota: No tenemos porque especificar la caché a utilizar en cada consulta que queramos cachear. Recordar la entrada en la que hablamos sobre la configuración de Doctrine 2 donde había un método que especifica la caché a utilizar para las consultas DQL.

También podemos cachear la consulta DQL sin los datos que recupera, así evitamos procesarla en todas las peticiones. De esta forma se almacena en caché la consulta SQL generada y se utiliza directamente:

El driver para cachear las consultas DQL se pasa a la instancia de configuración de Doctrine o a cada una de las instancias Query similar a como se hace para cachear los resultados.

  • setQueryCacheDriver($driver) - Especifica el driver para cachear
  • setQueryCacheLifeTime($seconds = 3600) - Establece el tiempo que estará almacenada la consulta en caché.
  • expireQueryCache($bool) - Expira la consulta en caché.
  • getExpireQueryCache()
  • getQueryCacheDriver()
  • getQueryCacheLifeTime()

Podemos limitar el numero de resultados devueltos por una consulta DQL.

  • setMaxResults($maxResults) – Establece el número máximo de resultados devueltos.
  • setFirstResult($offset) – Establece la posición desde la que se recuperarán los resultados.

Hay que tener cuidado ya que especificar el número de resultados en consultas que utilizan FETCH JOINS producirá resultados inesperados ya que una entidad puede aparecer en muchas filas en este tipo de consultas.

Podéis encontrar la documentación exacta de la sintaxis de las consultas DQL en la referencia:
http://www.doctrine-project.org/docs/orm/2.0/en/reference/dql-doctrine-query-language.html#ebnf

Categorías:Bases de datos, Doctrine

Funciones DQL en Doctrine 2

25 marzo, 2011 Dejar un comentario

Entrada rápida (copia-pega) del capítulo doce de la documentación de Doctrine 2 donde se muestra la cantidad de funciones que dispone DQL y que podemos utilizar en nuestras consultas. Al final del mismo capítulo podéis encontrar toda la sintaxis del lenguaje DQL:

http://www.doctrine-project.org/docs/orm/2.0/en/reference/dql-doctrine-query-language.html#ebnf

Las siguientes funciones son soportadas en las cláusulas WHERE y HAVING.

  • ABS(expresión aritmética) – Retorna el valor absoluto de la expresión.
  • CONCAT(str1, str2) - Concatena dos strings.
  • CURRENT_DATE() - Retorna la fecha actual.
  • CURRENT_TIME() - Retorna la hora actual.
  • CURRENT_TIMESTAMP() – Retorna un timestamp de la fecha y hora actual.
  • LENGTH(str) - Retorno la longitud de un string.
  • LOCATE(needle, haystack [, offset]) – Retorna la primera ocurrencia de un substring en un string.
  • LOWER(str) - Retorna el string en minúsculas.
  • MOD(a, b) – Retorna a MOD b.
  • SIZE(collection) – Retorna el número de elementos de una colección.
  • SQRT(q) – Retorna la raíz cuadrada q.
  • SUBSTRING(str, start [, length]) – Retorna un substring de un string.
  • TRIM([LEADING | TRAILING | BOTH] [‘trchar’ FROM] str) - Quita determinados carácteres en un string, por defecto los espacios en blanco.
  • UPPER(str) – Retorna el string en mayúsculas.

Podemos utilizar operaciones aritméticas en las consultas DQL:

"SELECT person.salary * 1.5 FROM CompanyPerson person WHERE person.salary < 100000"

Podemos utilizar funciones de agregación, solo aplicables a las cláusulas WHERE y GROUP BY: AVG, COUNT, MIN, MAX, SUM.

Otras expressiones:

  • ALL/ANY/SOME – Utilizado en una cláusula WHERE seguido de una subconsulta SELECT.
  • BETWEEN a AND b y NOT BETWEEN a AND b – Utilizado para coincidir con rangos.
  • IN (x1, x2, …) y NOT IN (x1, x2, ..) – Utilizado para coincidir con un conjunto de valores.
  • LIKE .. y NOT LIKE .. – Utilizado para coincidir partes de un string usando el comodón %.
  • IS NULL y IS NOT NULL – Utilizado para consultar valores nulos o no nulos.
  • EXISTS y NOT EXISTS – Utilizados en conjunto con subconsultas SELECT.
Categorías:Bases de datos, Doctrine

Introduccion a DQL, Doctrine Query Language.

23 marzo, 2011 5 comentarios

Con vuestro permiso voy a saltarme el estudio de los capítulos diez y once de la referencia que tratan el sistema de eventos de Doctrine y el batch processing o como procesar una gran cantidad de entidades en Doctrine.

Vamos a ver otro de los capítulos más importantes, el capítulo doce dedicado al lenguaje DQL, siglas de Doctrine Query Language, o lo que es lo mismo, un lenguaje de consulta para nuestro modelo de objetos. Un grave error es confundir DQL con una forma de realizar SQL. DQL no es un lenguaje que empleamos para nuestro esquema relacional sino para realizar consultas a nuestras entidades, por lo que DQL no utiliza nombres de tablas, de columnas, ni funciones propias de un sistema gestor de bases de datos.

Tipos de consultas DQL:

DQL como lenguaje de consulta puede realizar consultas tipo SELECT, UPDATE y DELETE, pero no consultas tipo INSERT debido a que para garantizar la consistencia con tu modelo de objetos el encargado de realizar este tipo de consultas debe de ser el EntityManager a través de su método persist, como ya hemos visto en entradas anteriores.

Las consultas SELECT son la manera más poderosa de recuperar partes de tu modelo de objetos. Adicionalmente podemos recuperar entidades y sus asociaciones con una única consulta SQL lo que puede aumentar el rendimiento de tu aplicación en contraste con el método utilizaddo en anteriores entradas que usaba muchas consultas al navegar por las asociaciones.

Las consultas UPDATE y DELETE ofrecen una manera de ejecutar cambios en muchas entidades del modelo de objetos. Es un paso necesario ya que hasta ahora habíamos visto que para realizar un cambio en  una entidad teníamos que cargarla en memoria, y no nos vamos a poner a cargar en memoria todas las entidades que debemos actualizar. Imaginar que tuvieramos que cambiar toda una tabla de una base de datos que contiene más de un millón de registros.

Consultas SELECT

La cláusula SELECT de un consulta DQL especifica lo que aparece en su conjunto de resultados. Vamos a ver un ejemplo para seleccionar aquellos usuarios con una edad mayor a 20.

<?php
$query = $em->createQuery('SELECT u FROM MyProject\Model\User u WHERE u.age > 20');
$users = $query->getResult();

Vamos a examinar poco a poco la consulta:

  • u es el alias que utilizamos para referirnos a la entidad User. Poniendo este alias en la cláusula SELECT estamos especificando que queremos todas las instancias de la entidad User que coinciden con las condiciones.
  • La cláusula FROM especifica el nombre de la entidad seguido de un alias para esa misma entidad.
  • La expresión u.age se conoce como path expression (dejo el original en inglés). Este tipo de expresiones se construyen con el operador . y en este ejemplo se refiere a la propiedad age de la clase User.

El resultado de este consulta es una lista de objetos User donde todos los usuarios son mayores de 20 años.

Importante: La cláusula SELECT especifica las variables que se van a recuperar en el proceso de hydrating, si solo especificamos un alias recuperará la entidad entera, pero si solo nos interesa recuperar un conjunto de propiedades podemos utilizar la sintaxis u.name. DQL es un lenguaje muy poderoso, seguramente te quedes alucinado con la gran cantidad de funcionalidad que tiene, y todo sin acoplarte a tu esquema en la base de datos. Impresionante.

Joins:

Una consulta SELECT puede contener joins. Hay dos tipos de JOINS, Regular Joins y Fetch Joins:

  • Regular Joins: Los utilizamos para limitar los resultados y realizar cálculos de valores agregados.
  • Fecth Joins: Los utilizamos para recuperar también las entidades relacionadas e incluirlas en el proceso de hydrating de tal forma que después al acceder a sus entidades relacionadas no se tengan que recuperar de la base de datos.

Ejemplo de un regular join:

<?php
$query = $em->createQuery("SELECT u FROM User u JOIN u.address a WHERE a.city = 'Berlin'");
$users = $query->getResult();

Hemos cruzado la entidad User con la entidad Address para recuperar solamente los usuarios cuya dirección sea de Berlín. Como vemos, hemos utilizado la sintaxis u.address porque estamos accediendo a la entidad address relacionada con el usuario. En este ejemplo, la entidad User tiene una propiedad address que se habrá anotado con una anotación de algún tipo de asociación.

Ejemplo de un fecth join:

<?php
$query = $em->createQuery("SELECT u, a FROM User u JOIN u.address a WHERE a.city = 'Berlin'");
$users = $query->getResult();

En este ejemplo realizamos lo mismo que en el ejemplo anterior, pero además recuperamos la entidad address asociada a los usuarios que coincidan con el criterio que hemos utilizado. De esta forma incluimos las entidades relacionadas en el proceso de hydrating y Doctrine no tendrá que consultarlo en la base de datos si accedemos a ellas.

<?php
//Recuperar todos los usuarios
$query = $em->createQuery('SELECT u FROM MyProject\Model\User u');
$users = $query->getResult(); // array de objetos User

//Recuperar todos los ids de los usuarios CmsUser:
$query = $em->createQuery('SELECT u.id FROM CmsUser u');
$ids = $query->getResult(); // array de ids

//Recuperar todos los ids de los usuarios que han escrito un articulo. Se puede utilizar DISTINCT.
$query = $em->createQuery('SELECT DISTINCT u.id FROM CmsArticle a JOIN a.user u');
$ids = $query->getResult(); // array de ids

//Recuperar todos los articulos ordenados por el nombre de los usuarios que los han escrito. 
//Se puede utilizar ORDER BY.
$query = $em->createQuery('SELECT a FROM CmsArticle a JOIN a.user u ORDER BY u.name ASC');
$articles = $query->getResult(); // array de objetos CmsArticle

//Recuperar el nombre y apellidos de un CmsUser:
$query = $em->createQuery('SELECT u.username, u.name FROM CmsUser u');
$users = $query->getResults(); // array de nombres y apellidos
echo $users[0]['username']; //acceder al apellido
echo $users[0]['name']; //acceder al nombre

//Recuperar un ForumUser y su entidad Avatar relacionada:
$query = $em->createQuery('SELECT u, a FROM ForumUser u JOIN u.avatar a');
$users = $query->getResult(); // array de objetos ForumUser con la entidad asociada cargada
echo get_class($users[0]->getAvatar());

//Recuperar un CmsUser y todos los números de teléfono que tiene:
$query = $em->createQuery('SELECT u, p FROM CmsUser u JOIN u.phonenumbers p');
$users = $query->getResult(); // array de objetos CmsUser con los teléfonos asociados cargados
$phonenumbers = $users[0]->getPhonenumbers();

//Recuperar usuarios ForumUser ordenados ascendentemente por id:
$query = $em->createQuery('SELECT u FROM ForumUser u ORDER BY u.id ASC');
$users = $query->getResult(); // array de objetos ForumUser

//O ordenados descendentemente por id:
$query = $em->createQuery('SELECT u FROM ForumUser u ORDER BY u.id DESC');
$users = $query->getResult(); // array de objetos ForumUser

//Usando funciones de agregación:
$query = $em->createQuery('SELECT COUNT(u.id) FROM Entities\User u');
$count = $query->getSingleScalarResult();

//Con cláusula WHERE y parámetros posicionales:
$query = $em->createQuery('SELECT u FROM ForumUser u WHERE u.id = ?1');
$query->setParameter(1, 321);
$users = $query->getResult(); // array de objetos ForumUser

//Con cláusula WHERE y parámetros por nombre
$query = $em->createQuery('SELECT u FROM ForumUser u WHERE u.username = :name');
$query->setParameter(':name', 'Bob');
$users = $query->getResult(); // array de objetos ForumUser

//Con condiciones anidadas en  la cláusula WHERE
$query = $em->createQuery('SELECT u from ForumUser u WHERE (u.username = :name OR u.username = :name2) AND u.id = :id');
$query->setParameters(array(
    ':name' => 'Bob',
    ':name2' => 'Alice',
    ':id' => 321,
));
$users = $query->getResult(); // array de objetos ForumUser

//Con COUNT DISTINCT:
$query = $em->createQuery('SELECT COUNT(DISTINCT u.name) FROM CmsUser');
$users = $query->getResult(); // array de objetos ForumUser

//Con expresiones aritméticas en la cláusula WHERE:
$query = $em->createQuery('SELECT u FROM CmsUser u WHERE ((u.id + 5000) * u.id + 3) < 10000000');
$users = $query->getResult(); // array de objetos ForumUser

//Usando un LEFT JOIN para recuperar todos los ids de los usuarios y
// opcionalmente todos los ids de los artículos asociados.
$query = $em->createQuery('SELECT u.id, a.id as article_id FROM CmsUser u LEFT JOIN u.articles a');
$results = $query->getResult(); // array de ids de usuario y por cada usuario los ids de sus articulos

//Restringiendo la cláusula JOIN con condiciones adicionales
$query = $em->createQuery("SELECT u FROM CmsUser u LEFT JOIN u.articles a WITH a.topic LIKE '%foo%'");
$users = $query->getResult();

//Usando múltiples Fetch JOINs:
$query = $em->createQuery('SELECT u, a, p, c FROM CmsUser u JOIN u.articles a JOIN u.phonenumbers p JOIN a.comments c');
$users = $query->getResult();

//BETWEEN en la cláusula WHERE:
$query = $em->createQuery('SELECT u.name FROM CmsUser u WHERE u.id BETWEEN ?1 AND ?2');
$query->setParameter(1, 123);
$query->setParameter(2, 321);
$usernames = $query->getResult();

//Funciones DQL en la cláusula WHERE:
$query = $em->createQuery("SELECT u.name FROM CmsUser u WHERE TRIM(u.name) = 'someone'");
$usernames = $query->getResult();

//Usando IN()
$query = $em->createQuery('SELECT u.name FROM CmsUser u WHERE u.id IN(46)');
$usernames = $query->getResult();

$query = $em->createQuery('SELECT u FROM CmsUser u WHERE u.id IN (1, 2)');
$users = $query->getResult();

$query = $em->createQuery('SELECT u FROM CmsUser u WHERE u.id NOT IN (1)');
$users = $query->getResult();

//Usando la función DQL CONCAT():
$query = $em->createQuery("SELECT u.id FROM CmsUser u WHERE CONCAT(u.name, 's') = ?1");
$query->setParameter(1, 'Jess');
$ids = $query->getResult();

$query = $em->createQuery('SELECT CONCAT(u.id, u.name) FROM CmsUser u WHERE u.id = ?1');
$query->setParameter(1, 321);
$idUsernames = $query->getResult();

//EXISTS en cláusula WHERE con subconsultas
$query = $em->createQuery('SELECT u.id FROM CmsUser u WHERE EXISTS (SELECT p.phonenumber FROM CmsPhonenumber p WHERE p.user = u.id)');
$ids = $query->getResult();

//Seleccionar todos los  usuarios que son miembros de un grupo
$query = $em->createQuery('SELECT u.id FROM CmsUser u WHERE :groupId MEMBER OF u.groups');
$query->setParameter(':groupId', $group);
$ids = $query->getResult();

//Seleccionar todos los usuarios que tienen al menos un teléfono:
$query = $em->createQuery('SELECT u FROM CmsUser u WHERE SIZE(u.phonenumbers) > 1');
$users = $query->getResult();

//Seleccionar todos los usuarios que no tienen teléfono
$query = $em->createQuery('SELECT u FROM CmsUser u WHERE u.phonenumbers IS EMPTY');
$users = $query->getResult();

Viendo todos estos ejemplos nos damos cuenta de la potencia del lenguaje DQL. Podemos realizar practicamente todo lo que hacemos con SQL pero aplicado a nuestro modelo de objetos de nuestra aplicación.

Objetos parciales:

Por defecto cuando solo recuperamos un conjunto de propiedades asiladas en una consulta DQL los resultados recuperados son arrays y no objetos, pero podemos recuperar objetos parciales, de tal forma que la consulta devuelva el objeto con las propiedades que hemos seleccionado. De esta forma podemos utilizar todos los métodos de la entidad:

<?php
$query = $em->createQuery('SELECT partial u.{id, username} FROM CmsUser u');
$users = $query->getResult(); // array de objetos parciales CmsUser

Incluso lo podemos hacer con JOINS:

<?php
$query = $em->createQuery('SELECT partial u.{id, username}, partial a.{id, name} FROM CmsUser u JOIN u.articles a');
$users = $query->getResult(); // array de objetos parciales CmsUser

Consultas UPDATE

Las consultas DQL UPDATE se utilizan para modificar muchas entidades de nuestro modelo de objetos.

"UPDATE MyProject\Model\User u SET u.password = 'new' WHERE u.id IN (1, 2, 3)"

Referencias a entidades relacionadas solo es posible en la cláusula WHERE y usando subconsultas.

Consultas DELETE

Las consultas DQL DELETE tienen una sintaxis muy parecida a las consultas UPDATE:

"DELETE MyProject\Model\User u WHERE u.id = 4"

Se aplica la  misma restricción en las referencias a entidades relacionadas.

NOTA: Las consultas UPDATE y DELETE se ejecutan directamente en la base de datos por lo que las entidades que estén cargadas en el mapa del EntityManager no se sincronizan con los nuevos cambios. Se recomiendo invocar el método clear del EntityManager y volver a recuperar las nuevas instancias de cualquier entidad afectada.

 

Categorías:Bases de datos, Doctrine

Transacciones en Doctrine 2

17 marzo, 2011 1 comentario

Doctrine 2 utiliza el Unit Of Work para encolar todas las consultas SQL que tiene que ejecutar cuando se invoca el método flush(), sin embargo, también nos permite marcar las transacciones de dos formas diferentes:

Implicitamente

La primera forma es usar una transacción implicita de tal forma que la transacción está manejada por el EntityManager de Doctrine.

<?php
$user = new User;
$user->setName('George');
$em->persist($user);
$em->flush();

Explicitamente

La alternativa es utilizar el API Doctrine\DBAL\Connection directamente para controlar la transacción:

<?php

$em->getConnection()->beginTransaction();
try {

    $user = new User;
    $user->setName('George');
    $em->persist($user);
    $em->flush();
    $em->getConnection()->commit();
} catch (Exception $e) {
    $em->getConnection()->rollback();
    $em->close();
    throw $e;
}

No voy a entrar muy en detalle de como podemos utilizar transacciones en Doctrine 2 porque no creo que utilice ninguna en mi proyecto. Esta mini entrada es más que nada por curiosidad por si algún día tengo que utilizarlas. Podéis encontrar más información en el capítulo nueve de la referencia:

http://www.doctrine-project.org/docs/orm/2.0/en/reference/transactions-and-concurrency.html

Categorías:Bases de datos, Doctrine
Seguir

Recibe cada nueva publicación en tu buzón de correo electrónico.

Únete a otros 64 seguidores