Crear templates para consultas SQL

Los "templates" o "plantillas" nos sirven para disponer de una estructura base y agregarle solo la información que cambia o varía, evitando tener que volver a copiar código una y otra vez.

En el caso de HTML podríamos usar Smarty, lo que nos permite contar con archivos de extensión .tpl y separar el código de presentación del código que se encargaría de armar o recibir información de la lógica de nuestro sistema.

Me ha sucedido que en muchos proyectos relacionados con web y con PHP se tienen que trabajar intensamente con consultas sql, más allá del tema de usar o no una capa de abstracción para conectarse a la base de datos. En estas situaciones existe mucho trabajo del lado del rol de DBA para incorporar mucha lógica en el motor de base de datos, como ser restricciones de integridad, disparadores, vistas, etc. De la misma forma, muchas reglas del negocio se resuelven a través de complejas consultas SQL que escapan del entendimiento de muchos de los programadores tradicionales (cabe aclarar que un programador nace con un cerebro distinto al de un DBA; muchas veces odiamos a las bases de datos y todo lo relacionado con la teoría de conjuntos ;-)).

En estos casos conviene que las consultas vengan preparadas por el DBA y que el desarrollador solo tenga que ejecutarlas, en otros, directamente trabajar con vistas que nos oculten toda la complejidad del diseño de la estructura de datos. También se pueden aprovechar "procedimientos almacenados" para ejecutar consultas con solo entregar algunos parámetros.

Me ha sucedido en situaciones intermedias y menos complejas que me es útil manejar el concepto de templates pero aplicados a consultas SQL donde simplemente tengo almacenadas las consultas y les paso los parámetros que requieren para ejecutarse.

Un ejemplo sencillo sería:

SELECT * FROM usuarios WHERE id = 1

Donde si tuviera un código PHP tendría que hacer cosas como:

1  
<?php
2    
3  
function traerUsuario$id ){
4     
$sql "SELECT * FROM usuarios WHERE id = $id";
5  
6     
// ejecutar_sql( $sql );
7     // etc.
8    
9  
}
10    
11  
?> 



O alguna versión más POO como:

1  
<?php 
2  
3  
class Usuario{
4     function 
getId$id ){
5        
$sql "SELECT * FROM usuarios WHERE id = $id";
6        
// procesar consulta
7     
}
8  }
9  
10  
?> 



Ahora bien, ¿qué pasa si para todas mis tablas uso constantemente esta misma lógica, donde solo necesito cambiar el nombre de la tabla y mantengo siempre un campo id, y luego solo le paso distintos valores por parámetro?

Yo puedo procesar esta consulta como un template, armando el marco para la sentencia y luego sustituyendo las "marcas" por valores, simplemente así:

1  
<?php 
2  
3  $sql_one 
"SELECT * FROM %s WHERE id = %s ";
4  
$sql sprintf$sql_one"usuarios");
5  
6  
?>



Lo que sucede es que ahora tomará la sentencia sql y en cada %s ("s" por "String") sustituirá en orden por los valores que agregue a continuación, causando el resultado esperado:

$sql = "SELECT * FROM usuarios WHERE id = 1";

Y ahí podemos aumentar enormemente la complejidad de las consultas y podremos reutilizarlas todas las veces que queramos, más, si las tenemos almacenadas en un archivo aparte y que todo nuestro sistema las use. Llegado el caso, si hubiera que optimizarlas habría que modificar un solo archivo.

Otra cosa que se me ocurre es algo más parecido al patrón de diseño ActiveRecord, que nos solucionaría un poco la vida con la operación de nuestras clases de persistencia de objetos.

Por ejemplo, podría crearme una clase Persistente que fuera así (usaré PHP4 para demostrar que no hace falta gran uso del lenguaje para implementar la idea):

1  
<?php 
2  
3  
require_once( 'BaseDeDatos.class.php' )
4  
5  class 
Persistente {
6     var 
$id;
7     var 
$tabla;
8     var 
$orden;
9  
10     var 
$sql_one "SELECT * FROM %s WHERE id = %s ";
11     var 
$sql_all "SELECT * FROM %s ORDER BY %s ASC";
12  
13     function 
getId$id ){
14        
$sql sprintf$this->sql_one$this->tabla$id );
15  
16        
$bd = new BaseDeDatos();
17        
$datos $bd->traerTodo$sql );
18        
$this->cargarDatos$datos );
19     }
20     function 
cargarDatos$datos ){
21        foreach( 
$datos as $key => $it ){
22           
$this->$key $it;
23        }
24     }
25     function 
getAll(){
26        
$sql sprintf$this->sql_all$this->tabla$this->orden );
27  
28        
$bd = new BaseDeDatos();
29        
$datos $bd->traerTodo$sql );
30        return 
$datos
31     
}
32  } 
33  
34  
?>



¿Bien, y cómo se usaría? Me creo mi clase concreta de persistencia de objetos, por ejemplo Usuario, y simplemente defino la información particular de la misma:

1  
<?php 
2  
3  
require_once( 'Persistente.class.php' );
4  
5  class 
Usuario extends Persistente {
6  
7     function 
Usuario() {
8        
$this->tabla "usuarios";
9        
$this->orden "id";
10     }
11  } 
12  
13  
?>



Y listo, no debo decirle nada más. Ahora solo me basta usarla de esta forma:

Si quiero traerme a todos los usuarios

$usuario = new Usuario();
$datos = $usuario->getAll();


Y si quiero traerme solo a un usuario particular, basta con que la base de datos del usuario tenga los campos id, nombre y apellido para hacer dinámicamente lo siguiente:

$usuario = new Usuario();
$usuario->getId(1);

echo
"Mi nombre es {$usuario->nombre} y mi apellido
es {$usuario->apellido}"
;


Y lo más interesante es que en situaciones donde lo genérico puede quitarnos libertad de acción, simplemente con sobreescribir los valores de los atributos definidos en la clase Persistente pero del lado de la clase Usuario resolvemos algunas limitaciones:

1  
<?php 
2  
3  
require_once( 'Persistente.class.php' );
4  
5  class 
Usuario extends Persistente {
6  
7     function 
Usuario() {
8        
$this->tabla "usuarios";
9        
$this->orden "id";
10  
11        
// Agregamos un JOINs con otra tabla
12        
$this->sql_all "SELECT * FROM %s as u, 
telefonos as t WHERE u.id = t.idUsuario ORDER BY u.%s, t.id ASC"
;
13     }
14  }
15  
16  
?> 



Mmm... creo que hoy me fui muy lejos divagando... más adelante les comparto un ejemplo completo que incluye persistencia en todos sus casos (update, delete, etc).

Más información

- Manual oficial, comando sprintf

4 comentarios:

equintero dijo...

Gracias Enrique

Con ejemplos como este vamos entendiendo la cosa.

Detalle: se ve mejor asi
$sql = sprintf( $this->sql_one, $this->tabla, $this->id );

Cristian dijo...

Una pregunta ¿Qué diferencia hay entre
$sql_one = "SELECT * FROM %s WHERE id = %s ";
4 $sql = sprintf( $sql_one, "usuarios", 1 );

y

$tabla = "usuarios";
$id = 1;
$sql = "SELECT * FROM $tabla WHERE id = $id "

¿Qué gano con utilizar la función sprintf?

Gracias

Enrique Place dijo...

Estimado Ernesto:

Sí, es un ejemplo rápido y busca concentrarse en la idea general (ya hubo un colega que me dijo: "pero para qué explicas eso y no directamente ActiveRecord" ;-)).

En el ejemplo, ya que comentas esa parte, busca que si tú creas una clase como una fachada genérica para obtener los datos de la persistencia, cada vez que recibas un parámetro "id" obtendrás cargado en el objeto todos sus datos.

Tal vez se podrían hacer dos mejoras más, la que tu dices, pero con la opción de recibir por parámetro el id. Si no lo recibe, usa lo que tenga la clase definida en ese momento o lo sustituye con ese nuevo valor.

La otra mejora podría ser que existiera un constructor que ya recibe el id e internamente carga todos los datos para ese id, y no usar el getId, o lo usas pero es un método privado de la clase (ya que tal vez no quieres que se use externamente).

Yo creo que nunca hay solo un camino, y todo dependerá de tus necesidades.

Muchas gracias por los comentarios ;-)

Enrique Place dijo...

Estimado Christian:

> ¿Qué gano con utilizar la función
> sprintf?

En primera instancia estaba pensando este ejemplo de "template" para usarlo como un archivo aparte de consultas sql que no están embebidas en el propio código PHP, lo cual permiten mantenerlas aparte de la lógica del sistema.

Pero para tu pregunta concreta, en mi opinión te aporta además de un poco de claridad y un mecanismo genérico (no está asociado a nombre de variables, solo a posiciones a través de una cadena de texto vulgar y corriente), te permite de forma rápida especificar la representación de los tipos de cada uno de los valores que coloques en el template.

Los %s representan las "cadenas", pero por ejemplo un %x trata al valor como un entero pero lo representan en formato hexadecimal, etc.

Más información en el manual de sprintf.

Entradas populares