Louder Developer Zone
Articles, Tutorials and Tips from the community and Louder developers

Introducción a KEQL. Parte 1

Publish at Monday, April 05, 2010
By Core Developers - Louder

¿Qué es KEQL?

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.

Enlaces Externos

Tell friends about this article on social networks:



blog comments powered by Disqus

Previous: Integración de Kumbia Enterprise con Apache Cassandra Next: Use Firebird/Interbase with Kumbia Enterprise

Colaborate

Colaborate

We invite you to submit articles and tutorials to the Developer Zone.

Archive

  • Mayo 2009

Maybe you are interested

Added value to your Business.

Become a Solution Partner Louder Now.

Bring to the Open-Source retroactively..

Learn more about Shared Louder Labs