lunes, 2 de marzo de 2015

Generador de Consultas (Query Builder)

Si bien podemos acceder a nuestra base de datos a través de la capa básica proporcionada por Yii (DAO), en ocasiones puede ser algo tedioso y por ende susceptible de errores al escribir nuestra consulta directamente. Una alternativa es utilizar el Generador de Consultas.

Un ejemplo sería:
$query = (new \yii\db\Query())
    ->select('id, nombre')
    ->from('usuario')
    ->limit(10);
// Crea un commando
$command $query->createCommand();
// Ejecuta el commando
$rows $command->queryAll();


Métodos de Consulta

Como se notará, yii\db\Query es la parte principal que necesitamos. Query es en realidad sólo responsable de representar diversa información de consulta. La lógica real de construcción de consultas se realiza mediante yii\db\QueryBuilder cuando se llama al método createCommand(), y la ejecución de la consulta se la realiza por medio de yii\db\Command.

Para mayor comodidad, yii\db\Query proporciona un conjunto de métodos de consulta de uso común. Por ejemplo:


  • all(): construye la consulta, lo ejecuta y devuelve todos los resultados como una matriz.
  • one(): devuelve la primera fila del resultado.
  • column(): devuelve la primera columna del resultado.
  • scalar(): devuelve la primera columna de la primera fila del resultado.
  • exists(): devuelve un valor que indica si la consulta devuelve resultados.
  • count(): devuelve el resultado de una consulta tipo COUNT. Otros métodos similares sum($q), average($q), max($q), min($q) donde $q es un parámetro obligatorio para estos métodos y puede ser el nombre de la columna o expresión.


Constructor de Consultas (Building Query)

A continuación se verá como construir diferentes cláusulas de una sentencia SQL. Por sencillez, utilizaremos $query para representar el objeto yii\db\Query.

SELECT

Debemos especificar las columnas que queremos seleccionar y la tabla de la cual obtener los datos.

$query->select('id, nombre')
    ->from('usuario');

También lo podemos hacer mediante un arreglo, lo cual es útil cuando se trata de una selección dinámica.

$query->select(['id''nombre'])
    ->from('usuario');

Consejo: Es bueno acostumbrarse a utilizar arreglos. Esto se debe a que, expresiones como CONCAT(nombre, apellido) AS nombre_completo pueden contener comas, y el resultado puede ser una cadena separada en partes por comas, y obviamente ese no es el resultado que desearíamos.

Por otro lado, podemos utilizar prefijos de tablas o alias. Una manera sería, por ejemplo
usuario.id AS usuario_id
Y si utilizamos arreglos los podemos hacer algo similares a lo siguiente:
['usuario_id' => 'usuario.id', 'usuario_nombre' => 'usuario.nombre']

A partir de la versión 2.0.1 también se pueden especificar sub-consultas como columnas. Por ejemplo:

$subQuery = (new Query)->select('COUNT(*)')->from('user');
$query = (new Query)->select(['id''count' => $subQuery])->from('post');
// $query representa la siguiente consulta SQL:
// SELECT `id`, (SELECT COUNT(*) FROM `user`) AS `count` FROM `post`

Para seleccionar filas distintas, podemos utilizar distinct como en el ejemplo siguiente:

$query->select('user_id')->distinct()->from('post');

FROM

Para especificar de cuál(es) tabla(s) obtener los datos, utilizamos from():

$query->select('*')->from('usuario');

Podemos especificar varias tablas mediante una cadena separada por comas o una matriz. Los nombres de tabla pueden contener prefijos de esquema (por ejemplo, 'public.usuario' ) y/o alias de tabla (por ejemplo, 'usuario u' ). A continuación un ejemplo:

$query->select('u.*, p.*')->from(['usuario u''post p']);

Cuando las tablas se especifican como una matriz, también se puede utilizar las claves de matriz como los alias de tabla.

$query->select('u.*, p.*')->from(['u' => 'usuario''p' => 'post']);

También es posible especificar una sub-consulta como un objeto Query.

$subQuery = (new Query())->select('id')->from('usuario')->where('estado=1');
$query->select('*')->from(['u' => $subQuery]);

WHERE

Por lo general, los datos se seleccionan cumpliendo ciertos criterios. El Generador de consultas tiene algunos métodos útiles para dichas tareas. El más poderoso de los cuales es WHERE.

La manera más sencilla de utilizarlo es con una cadena.

$query->where('estado=:estado', [':estado' => $estado]);

Cuando se utiliza cadenas, hay que tener presente que se está realizando una consulta con parámetros, no una consulta por concatenación. La manera anterior es la correcta de utilizarlo, la siguiente no lo es:

$query->where("estado=$estado"); // Incorrecto!

También se lo puede hacer a través de params addParams:

$query->where('estado=:estado');$query->addParams([':estado' => $estado]);

Condiciones múltiples también pueden utilizarse de la siguiente manera:

$query->where([
    'estado' => 10,
    'tipo' => 2,
    'id' => [4815162342],
]);

Éste código es equivalente a

WHERE (`estado` = 10) AND (`tipo` = 2) AND (`id` IN (4, 8, 15, 16, 23, 42))

Cuando necesitamos comparar un campo con el valor NULL lo hacemos de la siguiente manera:

$query->where(['estado' => null]);

con lo que obtenemos

WHERE (`estado` IS NULL)

En cambio, si necesitamos una condición del tipo NOT NULL:

$query->where(['not', ['columna' => null]]);

También es posible utilizar sub-consultas:

$userQuery = (new Query)->select('id')->from('usuario');
$query->where(['id' => $userQuery]);

lo que nos genera el código

WHERE `id` IN (SELECT `id` FROM `usuario`)

Tenemos la posibilidad de utilizar operadores. El formato de uso es [operador, término_1, término_2, ...]. Los operadores que podemos especificar son:

  • and
  • or
  • between
  • not between
  • in
  • not in
  • like
  • or like
  • not like
  • or not like
  • exists
  • not exists
  • operador matemáticos de comparación (>, <, >=, <=)


A continuación un ejemplo de cómo sería su uso.

$query->select('id')
    ->from('user')
    ->where(['>=''id'10]);

Lo que equivale a

SELECT id FROM user WHERE id >= 10;

Para construir una condición dinámicamente es conveniente utilizar andWhere() y orWhere():

$stado 10;$termino_busqueda 'yii';
$query->where(['estado' => $estado]);
if (!empty($termino_busqueda)) {
    $query->andWhere(['like''titulo'$termino_busqueda]);
}

En este caso, termino_busqueda no es vacío, por lo que la consulta generada sería:

WHERE (`estado` = 10) AND (`titulo` LIKE '%yii%')

Constructor de Condiciones Filtro

Cuando construimos condiciones para filtrar nuestros datos, basados en lo que el usuario ingresa, queremos manejar adecuadamente los campos vacíos. Por ejemplo, un usuario puede llenar un formulario en el que no todos los campos son obligatorios, por lo que los campos vacíos no queremos que los tome en cuenta. Para lograr este objetivo podemos utilizar filterWhere().

// $username y $email son ingresados por un usuario
$query->filterWhere([
    'username' => $username,
    'email' => $email,
]);

El método filterWhere() es similar a Where(). La diferencia principal es que filterWhere() remueve los valores vacíos. Se considera campos vacíos a aquellos que tienen el valor null, una cadena vacía, una cadena de espacios en blanco, o un arreglo vacío.

Se puede utilizar andFilterWhere() y orFilterWhere() para añadir más condiciones.

ORDER BY

Para ordenar los datos se puede utilizar orderBy o addOrderBy:
$query->orderBy([
    'id' => SORT_ASC,
    'nombre' => SORT_DESC,
]);

GROUP BY y HAVING

Para añadir una cláusula GROUP BY a nuestra sentencia:

$query->groupBy('id, estado');

Si queremos añadir otro campo para ser agrupado:

$query->addGroupBy(['creado_el''actualizado_el']);

Para añadir una condición HAVING:

$query->having(['estado' => $estado]);

LIMIT y OFFSET

Para limitar el resultado a 10 filas:

$query->limit(10);

Para saltar 100 filas:

$query->offset(100);

JOIN

Para la cláusula JOIN tenemos los siguiente métodos:

  • innerJoin()
  • leftJoin()
  • rightJoin()


Por ejemplo:

$query->select(['usuario.nombre AS autor''post.titulo as titulo'])
    ->from('usuario')
    ->leftJoin('post''post.usuario_id = usuario.id');

Si nuestra base de datos no soporta alguno de los tipos de JOIN, podemos utilizar el método genérico join:

$query->join('FULL OUTER JOIN''post''post.usuario_id = usuario.id');

De la misma manera que FROM, podemos utilizar sub-consultas. Por ejemplo:

$query->leftJoin(['u' => $subQuery], 'u.id=autor_id');

UNION

En Yii primero debemos construir la primera consulta, luego construir la segunda, y por último unir los resultados. Por ejemplo:

$query = new Query();$query->select("id, categoria_id as tipo, nombre")->from('post')->limit(10);
$otroQuery = new Query();
$otroQuery->select('id, tipo, nombre')->from('usuario')->limit(10);
$query->union($otroQuery);

Consulta por lotes 

Cuando se trabaja con grandes cantidades de datos, métodos como yii\db\Query::all() no son adecuados, ya que requieren la carga de todos los datos en la memoria. Para mantener el requisito de memoria baja, Yii ofrece apoyo a la consulta por lotes. Una consulta por lotes hace uso de cursores de datos y recupera los datos en lotes.

Un ejemplo es el siguiente:

use yii\db\Query;
$query = (new Query())
    ->from('usuario')
    ->orderBy('id');

foreach ($query->batch() as $usuarios) {
    // $usuarios es un arreglo de 100 o menos filas de la tabla usuario
}
// o si se desea iterar fila por fila
foreach ($query->each() as $usuario) {
    // $usuario representa una fila de datos de la tabla usuario
}

El método yii\db\Query::batch()yii\db\Query::each() retornan un objeto yii\db\BatchQueryResult que puede ser implementado por la interfaz Iterator y por lo tanto se puede utilizar con foreach. Durante la primera iteración, se realiza una consulta SQL a la base de datos. Los datos se captan por lotes en las iteraciones restantes. Por defecto, el tamaño del lote es de 100, lo que significa que 100 filas de datos están siendo extraídas en cada lote. Se puede cambiar el tamaño de lote pasándolo como primer parámetro a los métodos batch() o each().

En comparación con  yii\db\Query::all(), la consulta por lotes sólo carga 100 filas de datos a la vez en la memoria. Si procesamos los datos y luego los descartamos de inmediato, la consulta por lotes puede ayudar a reducir el uso de memoria.



También te puede interesar:
Trabajando con bases de datos
DAO - Database Access Objects (Objetos de acceso a base de datos)
Active Record (Registro Activo)

lunes, 26 de enero de 2015

DAO - Database Access Objects (Objetos de acceso a base de datos)

Si hemos trabajando con la primera versión de Yii, recordaremos que Yii provee una API orientada al acceso de bases de datos relacionales. En esta versión el concepto se mantiene, pero se lo hace de una manera diferente.

El DAO de Yii está construido sobre la base de PDO (PHP Data Objects - Objetos de datos de PHP) , y el cual provee métodos más avanzados para el acceso a la base de datos, incluyendo Query Builder (Constructor de Consultas) y Active Record (Registro Activo).

Al utilizar el DAO de Yii, solo se requiere de la consulta SQL y arreglos PHP. El resultado, es que accedemos a la base de datos de una manera eficiente.

El DAO de Yii soporta las siguientes bases de datos:

  • MySQL
  • MariaDB
  • SQLite
  • PostgreSQL
  • CUBRID: versión 9.3 o superior.
  • Oracle
  • MSSQL: versión 2008 o superior.


Creando conexiones a bases de datos

Para acceder a una base de datos, se debe conectar a ella mediante la creación de una instancia de yii\db\Connection:

$db = new yii\db\Connection([
    'dsn' => 'mysql:host=localhost;dbname=example',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

Puesto que es muy seguro que necesitemos conectarnos a nuestra base de datos desde algunas partes de nuestro desarrollo, lo común es configurarlo como un componente de la aplicación:

return [
    // ...
    'components' => [
        // ...
        'db' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=example',
            'username' => 'root',
            'password' => '',
            'charset' => 'utf8',
        ],
    ],
    // ...];

Para acceder a la base de datos, se utiliza Yii::$app->db.

Dependiendo de la base de datos que utilicemos, depende el nombre de origen de datos (DSN). Por ejemplo:


  • MySQL, MariaDB: mysql:host=localhost;dbname=mibasededatos
  • SQLite: sqlite:/path/al/archivo/base_de_datos
  • PostgreSQL: pgsql:host=localhost;port=5432;dbname=mibasededatos
  • CUBRID: cubrid:dbname=demodb;host=localhost;port=33000
  • MS SQL Server (vía sqlsrv driver): sqlsrv:Server=localhost;Database=mibasededatos
  • MS SQL Server (vía dblib driver): dblib:host=localhost;dbname=mibasededatos
  • MS SQL Server (vía mssql driver): mssql:host=localhost;dbname=mibasededatos
  • Oracle: oci:dbname=//localhost:1521/mibasededatos


Si nos conectamos a través de ODBC hay que configurar la propiedad yii\db\Connection::$driverName de manera similar a:

 'db' => [
  'class' => 'yii\db\Connection' ,
  'driverName' => 'mysql' ,
  'dsn' => 'odbc:Driver={MySQL};Server=localhost;Database=test' ,
  'username' => 'root' ,
  'password' => '' , 
 ],

Obviamente, también es necesario especificar el usuario y clave.

Es importante conocer que, al configurar nuestra instancia de conexión a base de datos, la conexión no se establece hasta que ejecutemos nuestra primera consulta SQL o llamemos explícitamente al método open().

Ejecutando consultas SQL

Para ejecutar consultas SQL debemos seguir los siguientes pasos:

  1. Crear la consulta SQL con yii\db\Command;
  2. Especificar los valores de nuestros parámetros (opcional);
  3. Ejecutar la consulta llamando a uno de los métodos de yii\db\Command.


A continuación unos cuantos ejemplos:

$db = new yii\db\Connection(...datos_de_conexion...);

// retorna un conjunto de filas. cada fila es una matriz asociativa de nombres y valores de las columnas.
// un arreglo vacío es devuelto si no hay resultados
$posts $db->createCommand('SELECT * FROM post')
            ->queryAll();

// retorna una única fila (la primera)
// se retorna false si no hay resultados
$post $db->createCommand('SELECT * FROM post WHERE id=1')
           ->queryOne();

// retorna a única columna (la primera)
// un arreglo vacío es devuelto si no hay resultados
$titles $db->createCommand('SELECT title FROM post')
             ->queryColumn();

// retorna un escalar
// se retorna false si no hay resultados
$count $db->createCommand('SELECT COUNT(*) FROM post')
             ->queryScalar();

Nota: Para conservar la precisión, los datos obtenidos de bases de datos están representados como cadenas, incluso si los tipos de columna de base de datos correspondientes son numéricos.

Es posible que, exactamente después de establecer la conexión a nuestra base de datos, necesitemos realizar alguna consulta, como por ejemplo para ajustar la zona horaria o el juego de carácteres. Para ello, podemos utilizar el controlador de eventos yii\db\Connection::EVENT_AFTER_OPEN de manera similar a:

return [
    // ...
    'components' => [
        // ...
        'db' => [
            'class' => 'yii\db\Connection',
            // ...
            'on afterOpen' => function($event) {
                // $event->sender se refiere a la conexión a BD
                $event->sender->createCommand("SET time_zone = 'UTC'")->execute();
            }
        ],
    ],
    // ...
];

Parámetros

Cuando creamos un comando de base de datos desde un SQL con parámetros, debemos hacerlo utilizando el método de parámetros vinculantes para prevenir ataques de inyección SQL. Por ejemplo,

$post $db->createCommand('SELECT * FROM post WHERE id=:id AND status=:status')
           ->bindValue(':id'$_GET['id'])
           ->bindValue(':status'1)
           ->queryOne();

En la sentencia SQL, se puede incrustar uno o varios marcadores de posición (por ejemplo :id en el ejemplo anterior). Un marcador de posición de parámetro debe ser una cadena que comienza con dos puntos. A continuación, se puede llamar a uno de los métodos correspondientes para enlazar los valores de los parámetros:

  • bindValue(): enlaza con un valor de parámetro único
  • bindValues(): enlaza valores de parámetro múltiples en una sola llamada
  • bindParam(): similar a bindValue() pero soporta referencias a parámetros

Los siguientes ejemplos muestran las diferentes alternativas:

$params = [':id' => $_GET['id'], ':status' => 1];
$post $db->createCommand('SELECT * FROM post WHERE id=:id AND status=:status')
           ->bindValues($params)
           ->queryOne();
           $post $db->createCommand('SELECT * FROM post WHERE id=:id AND status=:status'$params)
           ->queryOne();

El enlace de parámetros es implementado a través de sentencias preparadas, lo que a más de ayudar a evitar ataques de inyección SQL también mejora el rendimiento mediante la preparación de una sentencia SQL una sola vez y ejecutarla varias veces con diferentes parámetros. Por ejemplo,

$command $db->createCommand('SELECT * FROM post WHERE id=:id');
$post1 $command->bindValue(':id'1)->queryOne();$post2 $command->bindValue(':id'2)->queryOne();

Debido a que bindParam() admite parámetros vinculantes por referencia, el código anterior también puede escribirse como el siguiente:

$command $db->createCommand('SELECT * FROM post WHERE id=:id')
              ->bindParam(':id'$id);
$id 1;$post1 $command->queryOne();
$id 2;$post2 $command->queryOne();

Ejecución de consultas No-Select

Hasta el momento hemos visto los métodos queryAlgoMas() que permiten realizar consultas a la base de datos a través de una sentencia del tipo select. Es posible que también requiramos ejecutar sentencias como UPDATE, DELETE, INSERT para lo cual podemos utilizar el método yii\db\Command::execute() de manera similar a la siguiente:

$db->createCommand('UPDATE post SET status=1 WHERE id=1')
   ->execute();

El método  yii\db\Command::execute() retorna el número de filas afectadas por la sentencia.

También podemos llamar a  insert(), update(), delete() para construir el correspondiente SQL indicando el nombre de la tabla, columnas y valores.

// INSERT (tabla, columnas y valores)
$db->createCommand()->insert('usuario', [
    'nombre' => 'Santiago',
    'edad' => 30,
])->execute();

// UPDATE (tabla, columnas y valores, condición)
$db->createCommand()->update('usuario', ['estado' => 1], 'edad > 30')->execute();
// DELETE (tabla, condición)
$db->createCommand()->delete('usuario''estado = 0')->execute();

Y si lo que requerimos es insertar múltiples filas se puede utilizar batchInsert(), lo cual es más eficiente al momento de insertar varias filas que de una en una.

// tabla, columnas, valores
$db->createCommand()->batchInsert('usuario', ['nombre''edad'], [
    ['Tomás'30],
    ['Lorena'20],
    ['Linda'25],
])->execute();



También te puede interesar:
Trabajando con bases de datos
Generador de Consultas (Query Builder)
Active Record (Registro Activo)