KEQL es la abreviatura de Kumbia Enterprise Query Language y como su nombre lo indica se trata de un lenguaje de
consulta que interactúa con el componente ActiveRecord y los modelos
de las aplicaciones.
Su objetivo es facilitar la consulta y generación de SQL que sea portable en el mismo sentido como
se utilizaría el lenguaje tradicinal SQL.
La vida sin KEQL
Kumbia Enterprise implementa el patrón MVC, esto significa que es posible utilizar modelos como abstracciones
de las tablas de la base de datos buscando mayor facilidad de desarollo y mantenibilidad de las aplicaciones.
Por ejemplo, en cierta aplicación la tabla "clientes" puede trabajarse como una clase "Clientes".
Del mismo modo la clase "Clientes" puede mapear una tabla cuyo nombre este definido por alguna otra convención, por ejemplo "cli_emp".
Al tener distintos nombres el uso de SQL directamente en el código hace que el modelo "Clientes" pierda un poco
de sentido al desprenderse de la abstracción que significaba para la arquitectura de la aplicación.
Según el planteamiento anterior el modelo se define así:
<php
class Clientes extends ActiveRecord {
public function initialize(){
$this->setSource("cli_emp");
}
}
Luego pudiesemos haber utilizado SQL directamente en nuestra aplicación por alguna razón ó
indicado el nombre de la tabla saltandónos el modelo:
//Obteniendo las facturas que no pertenezcan a algun tipo de cliente
$this->Facturas->find("cli_codigo NOT IN (SELECT codigo FROM cli_emp WHERE cli_emp.tipo = 'A')");
Ahora pensemos que por alguna otra razón la tabla que mapea el modelo "Clientes" ya no se llama
"cli_emp" sino "cli_datos" y que está ubicada en otro esquema ó base de datos llamada "basicas".
El modelo se ajustaría así:
<php
class Clientes extends ActiveRecord {
public function initialize(){
$this->setSchema("basicas");
$this->setSource("cli_datos");
}
}
Del lado del modelo no hubo problema, pero como se utilizó el nombre de la tabla
directamente en el código ahora es necesario recorrer toda la aplicación haciendo el cambio por el nuevo
nombre. Esto podría representar un costo de tiempo innecesario en desarrollo y afectar
la estabilidad del código en el peor de los casos.
Introducción al KEQL
KEQL está fuertemente inspirado en el Hibernate HQL y muchas de sus ideas son también válidas aquí.
El objetivo del KEQL es realizar complejas sentencias de consulta, actualización ó borrado
aprovechando los modelos y su definición de esta forma haciendo la aplicación mucho más mantenible.
El lenguaje KEQL está por encima de la sintaxis SQL de cada motor pero al ejecutarse
es convertido al lenguaje SQL nativo de la base de datos utilizada.
Ejemplos de KEQL
Para usar KEQL requerimos de la clase ActiveRecordQuery. Algunos ejemplos básicos de uso son los siguientes:
//Consultar las primeras 5 facturas ordenadas por fecha descendente
//que pertenezcan a los clientes creados en los últimos 3 meses
$fechaCreado = new Date();
$fechaCreado->diffMonths(3);
$facturas = ActiveRecordQuery::create()
->select("f.prefijo, f.numero, f.fecha_venta")
->from("Facturas f")
->join("Clientes c")
->where("c.fecha_creado >= ?", $fechaCreado)
->limit(5)
->orderBy("f.fecha_venta DESC");
El código anterior es totalmente portable y fácil de mantener a futuro:
La fecha es creada usando el componente Date
así no se acude a las operaciones de fechas de la base de datos ni al formato de fecha que maneje el motor
Se usan los modelos "Clientes" y "Facturas" sin importar que tablas y bases de datos sean las que mapeen las respectivas clases
Los parámetros de la clausula WHERE son más seguros ya que las comillas se agregan automáticamente
evitando ataques de inyección de SQL
No fue necesario indicar los campos que arman la relación entre "Facturas" y "Clientes" ya
que esto se toma de la definición del modelo.
Se agrega un LIMIT de 5 registros que funciona en cualquier motor
Al usar SQL directamente el orden del ORDER BY y el LIMIT puede variar
dependiendo del motor ó de su implementación, sin embargo en este
caso es transparente al desarrollador.
Otra forma de escribir la consulta anterior es:
<php
//Consulta usando KEQL directamente
$keql = "SELECT f.prefijo, f.numero, f.fecha_venta
FROM Facturas f, Clientes c
WHERE c.fecha_creado >= ?
LIMIT 5
ORDER BY f.fecha_venta";
$clientes = ActiveRecordQuery::fromKEQL($keql, $fechaCreado);
En el ejemplo anterior se hace uso de lenguaje KEQL pero en vez de tablas
se usan los modelos y se omiten las condiciones que hacen la relación entre las 2 entidades
ya que estas se agregan automáticamente. Ambas opciones son válidas, aunque en búsqueda
de un mejor rendimiento se recomienda usar la primera forma.
Hacer diferentes tipos de Joins
Cuando hay que tomar datos de varias entidades (tablas, vistas, sinónimos, etc) el patrón de diseño
ActiveRecord puede parecer insuficiente. Para suplir esta necesidad Kumbia Enterprise ya había implementado
desde versiones anteriores el
ActiveRecordJoin. Con él era posible unir registros de diferentes entidades
apartir de sus llaves primarias ó relaciones y generar un conjunto de registros que además aprovechase
los indices de la base de datos sin saltarse la abstracción de cada modelo.
//Listar los productos vendidos, su cantidad y la fecha en que se vendieron
$query = new ActiveRecordJoin(array(
"entities" => array("Facturas", "Productos", "FacturasDetalle"),
"fields" => array(
"{#Products}.nombre",
"{#Facturas}.fecha_venta",
"{#FacturasDetalle}.cantidad"
)
));
Como resultado se produce una sentencia SQL obtenido solo los registros que cumplieran
con todas las condiciones de relación de las entidades utilizadas:
SELECT productos.nombre, facturas.fecha_venta, facturas_detalle.cantidad FROM
facturas, productos, facturas_detalle WHERE
productos.id = facturas_detalle.productos_id AND
facturas.id = facturas_detalle.facturas_id
La sentencia SQL tenia una clausula WHERE que implementaba natural joins para obtener
solo los registros intersección de todas las entidades indicadas. Algunos motores
como Oracle ó
MySQL implementan planificadores que no generan
ninguna mejora en rendimiento al usar joins naturales ó left joins. Otros motores
como IBM DB2 e Informix
pueden producir algunas variaciones en rendimiento dependiendo si las condiciones se colocan en el WHERE ó en el FROM.
Con KEQL es posible utilizar todos los tipos de Joins sin importar si son ó no soportados
nativamente por motor de base de datos.
Los tipos de Joins que se pueden usar son:
Tipo
Descripción
Ejemplo SQL Ilustrativo
INNER JOIN
Es el más común de los JOINs y devuelve los registros
que estén en ambas tablas del JOIN. Se puede entender
como la intersección de los registros de las 2 tablas.
SELECT * FROM TablaA INNER JOIN TableB
ON TablaA.name = TablaB.name
LEFT JOIN
Devuelve el conjunto de todos los valores de la tabla izquierda con los
valores de la tabla de la derecha correspondientes, o devuelve un valor nulo
NULL en caso de no correspondencia. Al agregar una condición de campos nulos
de la tabla de la derecha se obtiene solo los que estén a la izquierda
que también estén a la derecha.
SELECT * FROM TablaA LEFT OUTER JOIN TablaB
ON TablaA.name = TablaB.name
WHERE TablaB.id IS NULL
RIGHT JOIN
Devuelve el conjunto de todos los valores de la tabla derecha con los
valores de la tabla de la izquierda correspondientes, o devuelve un valor nulo
NULL en caso de no correspondencia. Al agregar una condición de campos nulos
de la tabla de la izquierda se obtiene solo los que estén a la derecha
que también estén a la izquierda.
SELECT * FROM TablaA LEFT OUTER JOIN TablaB
ON TablaA.name = TablaB.name
WHERE TablaB.id IS NULL
NATURAL JOIN
Es como el INNER JOIN pero todas las condiciones se especifican
en la clausula WHERE
SELECT * FROM TablaA,TablaB
WHERE TablaA.name = TablaB.name
FULL OUTER JOIN
Devuelve los registros de tabla izquierda y derecha
aunque no tengan correspondencia en la otra tabla.
La tabla combinada tiene todos los registros de ambas tablas y
presenta valores nulos nulos para registros sin pareja.
Al agregar condiciones de nulos a ambos lados se obtiene
la union menos la intersección, que es más útil.
SELECT * FROM TablaA
FULL OUTER JOIN TablaB
ON TablaA.name = TablaB.name
WHERE TablaA.id IS NULL OR TablaB.id IS NULL
CROSS JOIN
Devuelve el producto de combinar todos los registros de la tabla
izquierda con la tabla derecha en la clausula WHERE. Puede variar
dependiendo del motor.
SELECT * FROM TablaA,TablaB
Los anteriores Joins se codificarían con KEQL de la siguiente forma. El SQL
del comentario se genera al usar un motor Oracle:
//SELECT a.* FROM tablea INNER JOIN tableb ON tableb.name = tablea.name
$query = ActiveRecordQuery::create()
->select("a.*")
->from("Tablea a")
->innerJoin("a.Tableb b");
//SELECT a.* FROM tablea LEFT OUTER JOIN tableb ON tableb.name = tablea.name WHERE tableb.name IS NULL
$query = ActiveRecordQuery::create()
->select("a.*")
->from("Tablea a")
->leftJoin("a.Tableb b");
//SELECT a.* FROM tablea RIGHT OUTER JOIN tableb ON tableb.name = tablea.name WHERE tablea.name IS NULL
$query = ActiveRecordQuery::create()
->select("a.*")
->from("Tablea a")
->rightJoin("a.Tableb b");
//SELECT a.* FROM tablea FULL OUTER JOIN tableb ON tableb.name = tablea.name WHERE tableb.name IS NULL OR tablea.name IS NULL
$query = ActiveRecordQuery::create()
->select("a.*")
->from("Tablea a")
->fullJoin("a.Tableb b");
//SELECT a.* FROM tablea,tableb WHERE tableb.name = tablea.name
$query = ActiveRecordQuery::create()
->select("a.*")
->from("Tablea a")
->join("a.Tableb b");
$query = ActiveRecordQuery::create()
->select("a.*")
->from("Tablea a")
->naturalJoin("a.Tableb b");
//SELECT a.* FROM tablea,tableb
$query = ActiveRecordQuery::create()
->select("a.*")
->from("Tablea a")
->crossJoin("a.Tableb b");
Joins de múltiples Tablas
Con KEQL también es posible hacer joins en más de una tabla. El siguiente ejemplo es mucho más completo:
Crear las Tablas
Se va a usar 3 tablas que no tienen las convenciones de Kumbia Enterprise y presentan llaves compuestas.
La siguiente es la estructura de las tablas:
//La tabla factura que tiene una llave compuesta
CREATE TABLE `facturas` (
`prefijo` char(5) NOT NULL,
`numero` int(11) NOT NULL,
`fecha_venta` date NOT NULL,
`cod_clientes` int(11) NOT NULL,
`estado` char(1) NOT NULL,
PRIMARY KEY (`prefijo`,`numero`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
//Detalle de la Factura: LLave primaria compuesta
CREATE TABLE `det_facturas` (
`prefijo` char(5) NOT NULL,
`numero` int(11) NOT NULL,
`linea` int(11) NOT NULL,
`cod_productos` int(11) NOT NULL,
`cantidad` int(11) NOT NULL,
`valor` decimal(16,2) NOT NULL,
PRIMARY KEY (`prefijo`,`numero`,`linea`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
//Productos
CREATE TABLE `produc_fac` (
`cod_productos` int(11) NOT NULL,
`nombre` varchar(70) NOT NULL,
`precio` decimal(16,2) NOT NULL,
`estado` char(1),
PRIMARY KEY (`cod_productos`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Aunque las tablas no tengan nombres con palabras completas se utilizan nombres
más familiares y se mapean a sus respectivas tablas. Cada clase debe ir en su respectivo
archivo:
// apps/default/models/facturas.php
class Facturas extends ActiveRecord {
public function initialize(){
//Relación 1-n a DetalleFacturas. Campos a la llave compuesta
$this->hasMany(
array("prefijo", "numero"),
"DetalleFacturas",
array("prefijo", "numero")
);
}
}
// apps/default/models/detalle_facturas.php
class DetalleFacturas extends ActiveRecord {
public function initialize(){
//Se mapea a la tabla "det_facturas"
$this->setSource("det_facturas");
//Relación n-1 a Facturas. Campos a la llave compuesta
$this->belongsTo(
array("prefijo", "numero"),
"Facturas",
array("prefijo", "numero")
);
//Relación n-1 a Productos
$this->belongsTo(
"cod_productos",
"Productos",
"cod_productos"
);
}
}
// apps/default/models/productos.php
class Productos extends ActiveRecord {
public function initialize(){
//Se mapea a la tabla "produc_fac"
$this->setSource("produc_fac");
//Relación 1-n a DetalleFacturas
$this->hasMany(
"cod_productos",
"DetalleFacturas",
"cod_productos"
);
}
}
Una vez definidas las relaciones entre las tablas usamos ActiveRecordQuery:
//Listar los productos vendidos, su cantidad y la fecha en que se vendieron
$consulta = ActiveRecordQuery::create()
->select("p.nombre, f.fecha_venta, d.cantidad")
->from("DetalleFacturas d")
->join("d.Facturas f")
->join("d.Productos p")
->execute();
foreach($consulta as $registro){
echo $registro->nombre, " ", $registro->fecha_venta;
}
La anterior consulta produce la siguiente sentencia SQL:
SELECT produc_fac.nombre,facturas.fecha_venta,det_facturas.cantidad
FROM facturas,det_facturas,produc_fac
WHERE det_facturas.prefijo = facturas.prefijo AND
det_facturas.numero = facturas.numero AND
produc_fac.cod_productos = det_facturas.cod_productos
Dependiendo del tipo de JOIN y el motor de base de datos la sentencia SQL puede variar.
Conclusiones
Con Kumbia Enterprise Query Language (KEQL) las aplicaciones se hacen más portables y
mantenibles
Gracias a esta nueva abstracción no es necesario lidiar con la forma en la que se implementan
las diversas extensiones y variaciones del SQL de cada RBDM.
En una segunda parte se explicarán más detalles del lenguaje KEQL disponible en Kumbia Enterprise
Framework 1.71.
Si tiene dudas puede obtener soporte de los desarrolladores
del framework en nuestro grupo de discusión en google groups.