Hágame caso, y evitará malgastar sus fuerzas: si quiere desarrollar una aplicación seria, apártese de LINQ para SQL. Al menos, de momento...
Llevo varios días descubriendo cosas que no me gustan, pero me he contenido por prudencia. El descubrimiento que ha puesto el último clavo sobre el ataúd del invento tiene que ver con la forma en que se manejan las relaciones maestro/detalles. Pongamos por caso que tenemos dos tablas relacionadas, como Clientes y Pedidos:
LINQ para SQL crea dos clases, con un formato predecible:
public partial class Customer
{
public EntitySet<Order> Orders { ... }
}
public partial class Order
{
public Customer Customer { ... }
}
¿En qué momento se leen los pedidos de un cliente? Por omisión, no cuando se crea el cliente; ni siquiera cuando se accede a la propiedad Orders. Hay que intentar hacer algo con el contenido de la colección para que se dispare la consulta SQL correspondiente, que se parecerá a la siguiente:
select * from orders
where CustomerID = @CustomerID
Hasta aquí, todo es esperable, predecible y aceptable... suponiendo que estamos creando una aplicación con la vetusta arquitectura cliente/servidor. Si vamos a tener cuatro gatos conectados a un servidor local de SQL Server (pues LINQ para SQL sólo soporta, de momento, SQL Server), las cosas nos irán bien. Pero para eso no hacía falta gastar dinero en una subscripción anual a la MSDN. Cómprese un Delphi 7 de segunda o tercera mano, o incluso un Visual Basic "clásico", y sea feliz.
Pero, ¿qué pasa con la programación seria? Cuando usted se trae los datos de un grupo de clientes a la capa de presentación, tiene dos opciones, teóricamente: la carga incremental, o por demanda, de sus pedidos, o traer todos los pedidos en la misma operación que trae el grupo de clientes. En mi libro particular de estilo, la carga "por demanda" no sería aceptable. Cuando alguien pide clientes y sus pedidos, es porque realmente necesita ambas entidades, y en programación multicapas, el pecado mortal de necesidad se llama round-trip; es decir, viaje de ida y vuelta al servidor de capa intermedia. Pero mantengamos las opciones abiertas, y supongamos que cada técnica tiene su uso. Pues bien:
- LINQ para SQL no permite implementar la carga por demanda cuando hay una capa intermedia. Para transmitir un registro de cliente, hay que "serializarlo", y eso implica serializar sus pedidos. Adiós lectura incremental de los pedidos.
- Y ahora viene lo peor: supongamos que ya estamos resignados a no usar la carga por demanda. El caso es que LINQ para SQL implementa la lectura, en esos casos, de manera asquerosamente ineficiente.
Aquí aparecerá el listo de siempre, que alegará la existencia de unas DataLoadOptions que permitirían desactivar la carga por demanda. En efecto, LINQ para SQL ofrece algo así:
DataLoadOptions dlo = new DataLoadOptions();
dlo.LoadWith<Customer>(c => c.Orders);
dataContext.LoadOptions = dlo;
Pero, si se molestase en comprobar qué es lo que realmente hace dicha opción, comprobaría que, si se leen cuatrocientos clientes, esta opción provocaría que el servidor de capa intermedia lanzase otras cuatrocientos consultas SQL para leer individualmente los pedidos de cada cliente. Sí, es verdad que esta avalancha se produciría de golpe, en vez de incrementalmente...
Las cosas son buenas o malas en comparación con otras. ¿Cómo se resuelve este problema en ADO.NET con los conjuntos de datos de toda la vida? Muy simple: usted sólo necesita lanzar dos consultas, y es ADO.NET quien se encarga de tejer la relación en memoria, una vez que dispone de todos los registros. ¿Quiere, por ejemplo, los clientes de Madrid y sus pedidos? En un mismo comando (¡para ahorrar aún más!) se incluirían dos consultas:
select * from Customers
where City = "Madrid";
select * from Orders
where CustomerID in (
select CustomerID from Customers
where City = "Madrid")
A SQL Server se le da muy bien responder a este tipo de consultas, a pesar de su aparente complejidad. Y no le digo nada sobre recuperar los clientes que hicieron pedidos la semana anterior: la ganancia comparativa sería escandalosa.
¿Conclusión? Pues que LINQ para SQL no está preparado para la vida real. Es un juguetito con ínfulas, que a la hora de la verdad se queda muy corto... y es lamentable que la comunidad de programadores no esté diciéndolo en voz alta. No se trata sólo del problema aquí explicado: hay que sumar los problemas que ya la propia Microsoft explica para el funcionamiento del sistema en tres capas. Por ejemplo: los valores originales de cada registro se almacenan en el contexto, y al serializar un registro se pierden, no existe una funcionalidad comparable al Merge de los conjuntos de datos...
Es cierto que Microsoft se reserva eso del ADO.NET Entity Framework para febrero o marzo (es significativo que hayan publicado Visual Studio 2008 sin esperar por él). Habrá que esperar al producto final antes pronunciarse. Pero cada vez creo menos en la bondad de los ORMs: tengo razones para esperar muy poco de este tipo de sistemas. De todos modos, seré prudente: ya veremos qué tal se comporta el nuevo invento. A lo mejor esta es la vez en que un ORM acierta por primera vez en la Historia...
... y que ahora alguien me diga cuál es la urgencia para aprender a usar LINQ. ¿Para aplicarlo al famoso LINQ para SQL?
Mucho cuidado: no estoy diciendo que no merezca pasarse a VS2008. Todo lo contrario, ¡pásese urgentemente a la nueva versión! Hay suficientes mejoras como para merecer la pena, y la principal de ellas, en mi humilde opinión, es la nueva técnica de generación de clases para conjuntos de datos, que facilita enormemente la creación de aplicaciones en tres capas.
Etiquetas: ADO.NET, C#, LINQ, OOP, SQL