viernes, 24 de abril de 2015

Acceso a datos

Como se había mencionado anteriormente, los datos traídos de la base de datos se alamcenan en instancias Active Record, y cada fila del resultado de la consulta corresponde a una sola instancia Active Record. Se puede acceder a los valores de columna mediante el acceso a los atributos de las instancias Active Record, por ejemplo,

// "id" y "correo" son los nombres de columna de la tabla "cliente"
$cliente Cliente::findOne(123);$id $cliente->id;$correo $cliente->correo;

Es importante tomar en cuenta que no deberíamos re-declarar ninguno de los atributos de Active Record ya que los define Yii de manera automática.

Transformación de datos

Sucede a menudo que los datos que se ingresan y/o se muestran están en un formato diferente del utilizado en el almacenamiento de los datos en una base de datos. Por ejemplo, supongamos que la fecha de nacimiento de los clientes está almacenados como marcas de tiempo UNIX (aunque no es un buen diseño), mientras que en la mayoría de los casos lo ideal sería manipular las fechas en un formato más legible como 'yyyy/mm/dd' . Para lograr este objetivo, se pueden definir métodos de transformación de datos en la clase Active Record del cliente como se muestra a continuación:

class Cliente extends ActiveRecord{
    // ...

    public function getBirthdayText()
    {
        return date('Y/m/d'$this->birthday);
    }
    
    public function setBirthdayText($value)
    {
        $this->birthday strtotime($value);
    }
}

Ahora, en lugar de acceder a $customer->birthday (que nos mostraría una marca de tiempo), accedemos a $customer->birthdayText (que nos presentará la fecha en formato yyyy/mm/dd)

Recuperación de datos en matrices

Mientras que la recuperación de datos en términos de objetos Active Record es conveniente y flexible, no siempre es deseable cuando se tiene que traer una gran cantidad de datos debido al uso grande de memoria. En este caso, podemos recuperar datos utilizando arreglos PHP llamando asArray() antes de ejecutar un método de consulta:

// retorna todos los clientes
// cada cliente es retornado como un arreglo asociativo
$clientes Cliente::find()
    ->asArray()
    ->all();

Nota: Si bien este método ahorra memoria y mejora el rendimiento, está más cerca de la capa de abstracción de la base de datos y por ende se perderá la mayor parte de las características de Active Record. Una distinción muy importante radica en el tipo de datos de los valores de columna. Cuando retornan los datos en los casos de Active Record, los valores de columna serán asignados automáticamente en función de los tipos de columna reales; por otra parte, cuando retornan datos en arreglos, los valores de columna serán cadenas (ya que son el resultado de PDO sin ningún procesamiento), independientemente de sus tipos de columna reales.

Recuperación de datos en lotes

En la entrada Generador de Consultas (Query Builder), hemos visto que podemos utilizar la consulta por lotes para minimizar el uso de memoria al consultar una gran cantidad de datos de la base de datos. Podemos utilizar la misma técnica en Active Record. Por ejemplo,

// extrae 10 cliente de una sola vez
foreach (Cliente::find()->batch(10) as $clientes) {
    // $clientes es un arreglo de 10 o menos objetos Cliente
}

// extrae 10 clientes a la vez e itera uno por uno
foreach (Cliente::find()->each(10) as $cliente) {
    // $cliente es un objeto Cliente
}

// consulta en lotes con carga lenta
foreach (Cliente::find()->with('ordenes')->each() as $cliente) {
    // $cliente es un objeto Cliente
}



También te puede interesar:
Consulta de Datos
Almacenamiento de Datos
Active Record (Registro Activo)

miércoles, 22 de abril de 2015

Consulta de Datos

Después de haber declarado una clase Active Record,

namespace app\models;

use yii\db\ActiveRecord;

class Cliente extends ActiveRecord{
    const ESTADO_INACTIVO 0;
    const ESTADO_ACTIVO 1;
    
    /**
     * @Retorna una cadena con el nombre de la tabla asociada a ésta clase ActiveRecord.
     */
    public static function tableName()
    {
        return 'cliente';
    }
}

podemos utilizarla para consultar datos de la tabla de base de datos correspondiente. El proceso se lo realiza normalmente en los tres pasos siguientes:

  1. Crear un nuevo objeto de consulta llamando al método yii\db\ActiveRecord::find();
  2. Construir el objeto de consulta llamando a cualquiera de los métodos de construcción de consultas;
  3. Llamar a un método de consulta para recuperar los datos en términos de instancias de Active Record.

A continuación algunos ejemplos:

// retorna un único cliente cuyo ID es 123
// SELECT * FROM `cliente` WHERE `id` = 123
$cliente Cliente::find()
    ->where(['id' => 123])
    ->one();

// retorna todos los cliente activos ordenados por su ID
// SELECT * FROM `cliente` WHERE `estado` = 1 ORDER BY `id`
$cliente Cliente::find()
    ->where(['estado' => Cliente::ESTADO_ACTIVO])
    ->orderBy('id')
    ->all();

// retorna el número de clientes activos
// SELECT COUNT(*) FROM `cliente` WHERE `estado` = 1
$conteo Cliente::find()
    ->where(['estado' => Cliente::ESTADO_ACTIVO])
    ->count();

// retorna todos los clientes en un arreglo de clientes
// SELECT * FROM `customer`
$clientes Cliente::find()
    ->indexBy('id')
    ->all();

En la parte anterior, $cliente es un objeto Cliente, mientras que $clientes (en plural) es un conjunto de objetos Clientes. Todos ellos se rellenan con los datos recuperados de la tabla cliente.

Puesto que yii\db\ActiveQuery se extiende de yii\db\Query, podemos utilizar todos los métodos de construcción de consultas y métodos de consulta descritos en la sección Generador de Consultas (Query Builder).

Es una tarea común consultar por los valores de la clave primaria o un conjunto de valores de columna, por lo que Yii ofrece dos métodos de acceso directo para este propósito:

  • yii\db\ActiveRecord::findOne(): devuelve una sola instancia Active Record con la primera fila del resultado de la consulta.
  • yii\db\ActiveRecord::findAll(): devuelve un conjunto de instancias de Active Record con todos los resultados de la consulta.


Ambos métodos pueden recibir en sus parámetros los siguientes formatos:

  • un valor escalar: el valor es tratado como el valor deseado de la clave primaria a ser buscado. Yii determinará automáticamente qué columna es la columna de clave primaria mediante la lectura de la información del esquema de base de datos.
  • un arreglo de valores escalares: el arreglo se trata como los valores de la clave primaria deseados que se buscarán.
  • un arreglo asociativo: las claves son los nombres de columna y los valores son los correspondientes valores de las columnas deseadas a ser buscados.

Por ejemplo:

// retorna un único cliente cuyo ID es 123
// SELECT * FROM `cliente` WHERE `id` = 123
$clienteCliente::findOne(123);

// retorna cliente cuyos ID son 100, 101, 123 o 124
// SELECT * FROM `cliente` WHERE `id` IN (100, 101, 123, 124)
$clientes Cliente::findAll([100101123124]);

// retorna un cliente activo cuyo ID is 123
// SELECT * FROM `cliente` WHERE `id` = 123 AND `estado` = 1
$cliente Cliente::findOne([
    'id' => 123,
    'estado' => Cliente::ESTADO_ACTIVO,
]);

// retorna todos los clientes inactivos
// SELECT * FROM `cliente` WHERE `estado` = 0
$customer Cliente::findAll([
    'estado' => Cliente::ESTADO_INACTIVO,
]);

Se debe tomar en cuenta que ni yii\db\ActiveRecord::findOne(), ni yii\db\ActiveQuery::one() añadirán LIMIT 1 a la sentencia generada. Si sabemos que la consulta va a retornar más de un registro, debemos añadirlo de manera explícita para mejorar el rendimiento, por ejemplo

Cliente::find()->limit(1)->one()

En lugar de utilizar los métodos de consulta, podemos sobrescribir la sentencia sql y obtener los datos en un Active Record. Para ello debemos llamar al método yii\db\ActiveRecord::findBySql() de manera similar a:

// retorna todos los clientes inactivos
$sql 'SELECT * FROM cliente WHERE estado=:estado';
$clientes Cliente::findBySql($sql, [':estado' => Cliente::ESTADO_INACTIVO])->all();

No debemos llamar a métodos adicionales luego de llamar a findBySql() pues serán ignorados.


También te puede interesar:
Acceso a Datos
Almacenamiento de Datos
Active Record (Registro Activo)


Active Record (Registro Activo)

Active Record proporciona una interfaz orientada a objetos para acceder y manipular los datos almacenados en bases de datos. Una clase Active Record está asociada con una tabla de base de datos, una instancia Active Record corresponde a una fila de esa tabla, y un atributo de una instancia de Active Record representa el valor de una columna en particular en la fila. En lugar de escribir sentencias SQL, podemos acceder a los atributos de Active Record y llamar a métodos Active Record para acceder y manipular los datos almacenados en las tablas de base de datos.

Por ejemplo, supongamos que Cliente es una clase Active Record que se asocia con la tabla de clientes y nombre es una columna de la tabla de clientes. Para insertar una nueva fila en la tabla Cliente escrbimos algo similar a:

$cliente = new Cliente();
$cliente->nombre 'Juan';
$cliente->save();


El código anterior es equivalente a usar la siguiente declaración SQL para MySQL, que es menos intuitiva, más propensa a errores, y podemos tener incluso problemas de compatibilidad si utilizamos otro tipo de base de datos:

$db->createCommand('INSERT INTO `cliente` (`nombre`) VALUES (:nombre)', [
    ':nombre' => 'Juan',
])->execute();

Yii proporciona el soporte Active Record para las siguientes bases de datos relacionales:

  • MySQL 4.1 o superior: via yii\db\ActiveRecord
  • PostgreSQL 7.3 o superior: via yii\db\ActiveRecord
  • SQLite 2 y 3: via yii\db\ActiveRecord
  • Microsoft SQL Server 2008 o superior: via yii\db\ActiveRecord
  • Oracle: via yii\db\ActiveRecord
  • CUBRID 9.3 o superior: via yii\db\ActiveRecord (Se debe tener en cuenta que debido a un error en la extensión PDO, valores ente comillas no funcionan adecuadamente, por lo que se requiere tanto el cliente como el servidor de CUBRID 9.3)
  • Sphinx: via yii\sphinx\ActiveRecord, requiere la extensión the yii2-sphinx
  • ElasticSearch: via yii\elasticsearch\ActiveRecord, requiere la extensión the yii2-elasticsearch


Además, Yii también admite el uso de Active Record con las siguientes bases de datos NoSQL:

  • Redis 2.6.12 o superior: vía yii\redis\ActiveRecord, requiere la extensión yii2-redis
  • MongoDB 1.3.0 o superior: vía yii\mongodb\ActiveRecord, requiere la extensión yii2-mongodb


Declarando Clases Active Record

Una clase Active Record se declara extendiendo yii\db\ActiveRecord. Debido a que cada clase Active Record se asocia con una tabla de base de datos, en esta clase se debe sobrescribir el método tableName() para especificar a qué tabla la clase está asociada.

En el siguiente ejemplo, declaramos una clase Cliente asociada a la tabla de base de datos cliente:

namespace app\models;

use yii\db\ActiveRecord;

class Cliente extends ActiveRecord{
    const ESTADO_INACTIVO 0;
    const ESTADO_ACTIVO 1;
    
    /**
     * @Retorna una cadena con el nombre de la tabla asociada a ésta clase ActiveRecord.
     */
    public static function tableName()
    {
        return 'cliente';
    }
}

Las instancias Active Record son consideradas como modelos. Por esta razón las colocamos bajo el nombre de espacio app\models.

Y puesto que, yii\db\ActiveRecord se extiende de yii\base\Model, hereda todas las características, tales como atributos, reglas de validación, serialización de datos, entre otros.

Conexión a Bases de Datos

De forma predeterminada, Active Record utiliza el componente de aplicación db como la conexión de base de datos para acceder y manipular los datos de la base de datos. El componente db se lo configura de manera similar a:

return [
    'components' => [
        'db' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=testdb',
            'username' => 'demo',
            'password' => 'demo',
        ],
    ],
];

Si requerimos de una conexión de base de datos diferente, debemos sobrescribir el método getDb():

class Cliente extends ActiveRecord{
    // ...

    public static function getDb()
    {
        // utiliza el componente "db2"
        return \Yii::$app->db2;  
    }
}



También te puede interesar:
Trabajando con bases de datos
DAO - Database Access Objects (Objetos de acceso a base de datos)
Consulta de Datos

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)