LINQ to SQL (III): Consultas compiladas, ejecución SQL y procedimientos almacenados


Compilación de consultas

En ocasiones podemos encontrarnos con consultas LINQ cuyo coste computacional puede ser alto, haciendo que el sistema se sobrecargue. LINQ to SQL proporciona algunas herramientas que mitigan, dentro de lo posible, el impacto de este tipo de operaciones sobre el rendimiento. Una de ellas es la compilación de consultas.

Es posible precompilar una consulta LINQ, de modo que durante su primera invocación realice un plan de ejecución que pueda mantenerse cacheado para ser reutilizado posteriormente. Así, la siguiente consulta:


            int idCliente = 2;

            var consulta = (from cliente in dbContext.Cliente
                            where (cliente.IdCliente == idCliente)
                            select cliente).FirstOrDefault();

Podría ser compilada y almacenada en una variable, que podrá ser reutilizada posteriormente como si fuese un método normal y corriente. Vemos, además, que la consulta puede parametrizarse, de modo que es posible asignarle cierto grado de generalización.


            var getClientePorId = CompiledQuery.Compile(
                (Testdb contexto, int id) =>
                    (from cliente in contexto.Cliente
                     where (cliente.IdCliente == id)
                     select cliente).FirstOrDefault()
                );

El uso de esta consulta compilada sería el siguiente:


            Cliente c = null;
            using (Testdb ctx = new Testdb(ConfigurationManager.ConnectionStrings["TestDb"].ConnectionString))
            {
                c = getClientePorId(ctx, idCliente);
            }

            if (c != null)
                Console.WriteLine(string.Format("CLIENTE: {0}", c.Nombre));

El resultado sería el siguiente:

Ejecución directa de SQL

Por contrapartida, quizás nos encontremos alguna vez con que es necesario ejecutar directamente una sentencia SQL (o procedimiento almacenado) indicando directamente el código SQL. Es una mala práctica que debe evitarse todo lo posible, pero es necesario conocer que es una opción en caso de que sea necesaria.

La ejecución de SQL se realiza mediante el método ExecuteQuery<Tipo> de la clase DataContext. El tipo es obligatorio, y sirve para modelar el listado resultante, indicando al compilador el tipo por el que estará compuesto el listado resultante. Un par de ejemplos serían los siguientes:


            Testdb dbContext = getDataContext();

            var identificadores = dbContext.ExecuteQuery<int>("Select IdCliente from Cliente where FechaNacimiento > {0}", new DateTime(1975, 01, 01));
            var clientes = dbContext.ExecuteQuery<Cliente>("Select * from Cliente where FechaNacimiento > {0}", new DateTime(1975, 01, 01));

            var filasModificadas = dbContext.ExecuteCommand("update Pedido set FechaPedido = {0} where IdPedido = {1}",
                new DateTime(2013, 11, 29), 10);

            Console.WriteLine("IDENTIFICADORES:");
            foreach (int id in identificadores)
                Console.WriteLine(string.Format("\t{0}", id));

            Console.WriteLine("\nCLIENTES:");
            foreach (var cliente in clientes)
                Console.WriteLine(string.Format("\t{0}\t{1}", cliente.IdCliente, cliente.Nombre));

            Console.WriteLine(string.Format("\nFilas modificadas: {0}", filasModificadas));

 

La iteración se realiza sobre enteros en el primer caso y sobre objetos de la clase Cliente sobre el segundo.

En cuanto al método ExecuteCommand, nos permite ejecutar operaciones INSERT, UPDATE y DELETE y obtener como valor de retorno el número de filas afectadas por la operación.

Procedimientos almacenados

Ya vimos en el artículo dedicado al mapeo objeto-relacional que es posible importar funciones y procedimientos almacenados mediante el DataContext Editor, y que éstos pueden ser invocados como una función normal y corriente:


            decimal? gastadoPorCliente3 = dbContext.Fn_Cliente_TotalGastado(3);
            if (gastadoPorCliente3 != null)
                Console.WriteLine(string.Format("El cliente 3 ha gastado un total de {0}", gastadoPorCliente3.Value));

El símbolo de interrogación después de decimal indica que se trata de un tipo nullable, un objeto especial que modela un número decimal con posibilidad de tener el valor null. Por ello, para acceder a su valor es necesario usar su propiedad Value, tal y como vemos en el código.

Los procedimientos almacenados mantienen un poco más de independencia respecto el control que LINQ establece sobre la base de datos, ya que no se ven afectados por algunas de sus características:

  • La ejecución es inmediata una vez que el procedimiento es invocado (eager) en lugar de posponerse hasta el momento en el que se usa el dato recuperado.
  • Los procedimientos almacenados no se ven afectados por la configuración del DataLoadOptions asociado al contexto.
  • El valor devuelto por defecto por un procedimiento almacenado no se corresponde con un POCO, sino que será un tipo anónimo constituido por los campos devueltos por el procedimiento. Aunque éstos coincidan exactamente, por ejemplo, con un Cliente, es necesario indicarle explícitamente que nos devuelva un objeto de ese tipo si es lo que realmente está devolviendo.
  • Por lo tanto, si deseamos que el tipo de dato devuelto por el procedimiento sea uno de nuestros datos estándar, será necesario indicarlo, bien en el editor de DataContext o bien editando el DataContext manualmente y cambiando el tipo devuelto por el procedimiento o función.

		[global::System.Data.Linq.Mapping.FunctionAttribute(Name="dbo.sp_Cliente_ProductosDesdeHasta")]
		public ISingleResult<Sp_Cliente_ProductosDesdeHastaResult> Sp_Cliente_ProductosDesdeHasta([global::System.Data.Linq.Mapping.ParameterAttribute(Name="IdCliente", DbType="Int")] System.Nullable<int> idCliente, [global::System.Data.Linq.Mapping.ParameterAttribute(Name="FechaDesde", DbType="DateTime")] System.Nullable<System.DateTime> fechaDesde, [global::System.Data.Linq.Mapping.ParameterAttribute(Name="FechaHasta", DbType="DateTime")] System.Nullable<System.DateTime> fechaHasta)
		{
			IExecuteResult result = this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), idCliente, fechaDesde, fechaHasta);
			return ((ISingleResult<Sp_Cliente_ProductosDesdeHastaResult>)(result.ReturnValue));
		}

En este caso, si lo que realmente queremos recuperar es un objeto Producto, podemos cambiar el tipo Sp_Cliente_ProductosDesdeHastaResult, creado al efecto para recuperar el valor del procedimiento y que tiene la siguiente forma:


	public partial class Sp_Cliente_ProductosDesdeHastaResult
	{

		private string _Nombre;

		private System.Nullable<System.DateTime> _FechaPedido;

		private string _Producto;

		private System.Nullable<int> _Cantidad;

		private System.Nullable<decimal> _Precio;

		private System.Nullable<decimal> _Total;

		public Sp_Cliente_ProductosDesdeHastaResult()
		{
		}

		[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Nombre", DbType="NVarChar(50)")]
		public string Nombre
		{
			get
			{
				return this._Nombre;
			}
			set
			{
				if ((this._Nombre != value))
				{
					this._Nombre = value;
				}
			}
		}

		[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_FechaPedido", DbType="DateTime")]
		public System.Nullable<System.DateTime> FechaPedido
		{
			get
			{
				return this._FechaPedido;
			}
			set
			{
				if ((this._FechaPedido != value))
				{
					this._FechaPedido = value;
				}
			}
		}

		[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Producto", DbType="NVarChar(100)")]
		public string Producto
		{
			get
			{
				return this._Producto;
			}
			set
			{
				if ((this._Producto != value))
				{
					this._Producto = value;
				}
			}
		}

		[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Cantidad", DbType="Int")]
		public System.Nullable<int> Cantidad
		{
			get
			{
				return this._Cantidad;
			}
			set
			{
				if ((this._Cantidad != value))
				{
					this._Cantidad = value;
				}
			}
		}

		[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Precio", DbType="Money")]
		public System.Nullable<decimal> Precio
		{
			get
			{
				return this._Precio;
			}
			set
			{
				if ((this._Precio != value))
				{
					this._Precio = value;
				}
			}
		}

		[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Total", DbType="Money")]
		public System.Nullable<decimal> Total
		{
			get
			{
				return this._Total;
			}
			set
			{
				if ((this._Total != value))
				{
					this._Total = value;
				}
			}
		}
	}

Por el siguiente código, sustituyéndolo por Producto y eliminando la clase anterior, que ya no nos hará falta.


		[global::System.Data.Linq.Mapping.FunctionAttribute(Name="dbo.sp_Cliente_ProductosDesdeHasta")]
		public ISingleResult<Producto> Sp_Cliente_ProductosDesdeHasta([global::System.Data.Linq.Mapping.ParameterAttribute(Name="IdCliente", DbType="Int")] System.Nullable<int> idCliente, [global::System.Data.Linq.Mapping.ParameterAttribute(Name="FechaDesde", DbType="DateTime")] System.Nullable<System.DateTime> fechaDesde, [global::System.Data.Linq.Mapping.ParameterAttribute(Name="FechaHasta", DbType="DateTime")] System.Nullable<System.DateTime> fechaHasta)
		{
			IExecuteResult result = this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), idCliente, fechaDesde, fechaHasta);
			return ((ISingleResult<Producto>)(result.ReturnValue));
		}

Con esto terminamos la parte de LINQ to SQL dedicada a la consulta de información. En los próximos artículos incidiremos un poco en las funciones de inserción, actualización y eliminación a través de esta tecnología.

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s