Funciones personalizadas en Doctrine ORM (Parte 2): funciones multiplataforma

En mi último post hablé acerca de cómo definir funciones personalizadas en Doctrine, que pueden utilizarse para realizar consultas en DQL. Aunque esta solución es perfectamente válida en la mayoría de casos de uso, tiene un problema fundamental: nuestra función personalizada sólo funciona en un motor de base de datos concreto (en el ejemplo que vimos era MySQL).

Una de las gran ventajas de DQL es que nos permite expresar consultas totalmente independientes del motor de base de datos utilizado, siempre y cuando utilicemos únicamente las funciones propias de Doctrine (que Doctrine sabe como traducir en código SQL nativo para todos los motores soportados). En el momento en el que definimos una función personalizada para un motor de base de datos concreto, todas las consultas que hagan uso de esa función sólo funcionarán en dicho motor. Así, por ejemplo, las consultas que utilizasen la función LPAD que definimos en el post anterior sólo funcionarían en MySQL (y como mucho en PostgreSQL) ya que por ejemplo SQL Server no dispone de ninguna función llamada LPAD (ni ninguna similar en realidad).

En muchas aplicaciones, que las consultas funcionen en un sólo motor de base de datos es suficiente, ya que es habitual que dicho motor sea establecido al inicio del proyecto y se utilice durante todo el ciclo de vida del mismo. Sin embargo, hay aplicaciones que deben poder funcionar sobre diferentes motores y por tanto, nuestras funciones personalizadas deben generar código SQL nativo apropiado para cada uno de ellos.

Hay varias formas de conseguir esto. A continuación vamos a ver una muy sencilla pero efectiva.

Definiendo funciones multiplataforma

La solución pasa por crear una clase base, que llamaremos PlatformAwareFunctionNode y que hereda de FunctionNode. Esta clase implementará la función getSql() (que utilizábamos en el ejemplo anterior para obtener el código SQL nativo de nuestra función) de la siguiente manera:

<?php namespace MyProject\DoctrineExtensions\Query; use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\QueryException; use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\Common\Inflector\Inflector; abstract class PlatformAwareFunctionNode extends FunctionNode { public function getSql(SqlWalker $sqlWalker) { $platformName = Inflector::classify(strtolower($sqlWalker->getConnection()->getDatabasePlatform()->getName()));
		$methodName   = 'getSqlFor' . $platformName;
 
		if(method_exists($this, $methodName))
			return $this->$methodName($sqlWalker);
		else
			throw QueryException::syntaxError("Function '{$this->name}' is not supported in platform '{$platformName}'. Please make sure that the " . get_class($this) . " class provides an implementation for function {$methodName}().");
	}
}

El método getSql() obtiene primero el nombre del motor de base de datos utilizado, mediante la llamada a la función $sqlWalker->getConnection()->getDatabasePlatform()->getName(). Posteriormente, comprueba si existe el método getSqlFor[Nombre motor base de datos](). Es decir, si estamos utilizando MySQL, comprobará si la clase define un método llamado getSqlForMysql(), que será el encargado de generar el SQL nativo para MySQL. Este método deberá ser proporcionado por la clase hija que herede de PlatformAwareFunctionNode. En caso de existir, se llama a dicho método y se devuelve su resultado. En caso contrario se lanza una excepción indicando al desarrollador que esta función no tiene implementación en el motor de base de datos utilizado.

De esta forma, para definir una función personalizada multiplataforma deberemos heredar de esta nueva clase PlatformAwareFunctionNode e implementar los métodos getSqlFor[Nombre motor base de datos]() para cada uno de los motores soportados. La función LPAD del ejemplo anterior quedaría de la siguiente manera:

<?php namespace MyProject\DoctrineExtensions\Query; use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\AST\Functions\FunctionNode; /** * LeftPad ::= "LPAD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" */ class LeftPad extends PlatformAwareFunctionNode { public $stringExpression    = null; public $lengthExpression    = null; public $padStringExpression = null; public function parse(\Doctrine\ORM\Query\Parser $parser) { $parser->match(Lexer::T_IDENTIFIER);
		$parser->match(Lexer::T_OPEN_PARENTHESIS);
		$this->stringExpression = $parser->ArithmeticPrimary();
		$parser->match(Lexer::T_COMMA);
		$this->lengthExpression = $parser->ArithmeticPrimary();
		$parser->match(Lexer::T_COMMA);
		$this->padStringExpression = $parser->StringPrimary();
		$parser->match(Lexer::T_CLOSE_PARENTHESIS);
	}
 
	public function getSqlForMysql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
	{
		return 'LPAD(' .
			$this->stringExpression->dispatch($sqlWalker) . ', ' .
			$this->lengthExpression->dispatch($sqlWalker) . ', ' .
			$this->padStringExpression->dispatch($sqlWalker) .
		')';
	}
 
	public function getSqlForPostgresql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
	{
		return 'lpad(' .
			$this->stringExpression->dispatch($sqlWalker) . ', ' .
			$this->lengthExpression->dispatch($sqlWalker) . ', ' .
			$this->padStringExpression->dispatch($sqlWalker) .
		')';
	}
 
	/* Añadir aquí implementaciones para otros motores */
}

En este ejemplo estamos definiendo esta función para los motores MySQL y PostgreSQL por medio de los métodos getSqlForMysql() y getSqlForPostgresql(). En este caso, el SQL devuelto por ambos métodos es muy similar, ya que la función LPAD existe en ambos motores y tiene los mismos parámetros y en el mismo orden. Para añadir soporte para nuevos motores simplemente habría que añadir sus métodos correspondientes. El método parse() sólo se implementa una vez (es igual para todos los motores), ya que nos interesa que la llamada a esta función en DQL sea la misma en todos los casos para que las consultas sean independientes del motor utilizado.

Conclusiones

En este post hemos visto una solución muy sencilla para definir funciones personalizadas en Doctrine que pueden funcionar en varios motores de bases de datos. Esto nos permite aprovechar al máximo las características propias de cada motor sin perder la capacidad de definir consultas independientes de los mismos.

The following two tabs change content below.

Mikel Pintor

Desarrollador web.

Latest posts by Mikel Pintor (see all)

Compartir: