5.1. Conceptos de LINQ Unidad V - Acceso a datos con LINQ 5.1. - Conceptos de LINQ Objetivo General. Usar la herramienta de acceso a datos de LINQ en conjunto con el lenguaje de desarrollo para hacer consultas directas a la base de datos y la administración centralizada de datos.
Objetivo Específico. Identificar patrones y conceptos generales del uso de LINQ para el acceso a datos.
Introducción.
El software es simple y se compone de dos cosas: código y datos. Escribir software no es tan simple, y una de las mayores actividades que implica es escribir código que maneje datos. Para escribir código, se puede escoger de entre una variedad de lenguajes de programación. El lenguaje seleccionado para una aplicación puede depender del contexto del negocio, de las preferencias del desarrollador, de las habilidades del equipo de desarrollo, del sistema operativo, o de políticas de la empresa. Cualquiera que sea el lenguaje seleccionado, en algún punto tendrá que tratar con datos. Estos datos pueden estar en archivos o en un disco, en tablas dentro de una base de datos, o documentos XML provenienter de la web, o a menudo se tiene que tratar con una combinación de todos ellos. Finalmente, administrar datos es un requerimiento para cualquier proyecto de software. Ya que trabajar con datos es una tarea común para los desarrolladores, se espera una plataforma de desarrollo de software rica como .NET Framework para proveer una forma fácil de hacerlo. .NET provee un amplio soporte para trabajar con datos. Sin embargo, algo aún tiene que lograrse: una mayor integración entre el lenguaje y los datos. Aquí es donde entran LINQ para Objetos, LINQ para XML y LINQ para SQL.
1. ¿Qué es LINQ?
Suponga que está escribiendo una aplicación usando .NET. Son altas las probabilidades de que en algún punto tendrá que usar persistencia de objetos a una base de datos, consultar la base de datos, y cargar los resultados de regreso a los objetos. El problema es que en muchos casos, al menos con las bases de datos relacionales, hay una brecha entre el lenguaje de programación y la base de datos. Se han hecho buenos intentos para proveer bases de datos orientadas a objetos, los cuales estarían más cercanos a plataformas orientadas a objetos y lenguajes de programación como C# y VB .NET. Sin embargo, después de todos estos años, las bases de datos relacionales aún son evasivar, y todavía tiene que lidiar con el acceso a datos y la persistencia en todos los programas. La motivación original detrás de LINQ era atender las dificultades técnicas y conceptuales encontradas, cuando se usan bases de datos con lenguajes de programación prog ramación .NET. Con LINQ, la intención de Microsoft fue proveer una solución para el problema del mapeo relacional de objetos, así como simplificar la interacción entre los objetos y los orígenes de datos. LINQ eventualmente evolucionó hacia un conjunto de herramientas de consulta de propósito general integradas al lenguaje. Este conjunto de herramientas puede ser usado para acceder datos que vienen de objetos en memoria (LINQ para Objetos), bases de datos (LINQ para bases de datos), XML (LINQ para XML), un sistema de archivos o cualquier otro origen.
1.1. Panorama general.
LINQ podría ser el enlace faltante entre el mundo de los datos y el mundo de los lenguajes de programación de propósitos generales. LINQ unifica el acceso a datos, cualquiera sea el origen de datos, y permite combinar datos de diferentes tipos de orígenes. Permite consultar y configurar operaciones, de manera similar a lo que las sentencias SQL ofrecen para las bases de datos. LINQ, sin embargo, integra consultas directamente dentro de los lenguajes .NET tales como C# y VB .NET a través de un conjunto de extensiones a esos lenguajes: LINQ significa Consulta Integrada al Lenguaje (Language-INtegrated Query). Antes de LINQ se tenía que lidiar con diferentes lenguajes como SQL, XML, o XPath junto con varias tecnologías y APIs como ADO .NET o System.Xml en cada aplicación escrita usando lenguajes de propósitos generales tales como C# o VB .NET. Y por supuesto que este enfoque tenía muchas desventajas (Similar a ordenar la cena en un idioma y pedir las bebidas en otro idioma). LINQ une varios mundos y ayuda a evitar las dificultades que usualmente se tendrían en el camino de un mundo a otro: usar XML con objetos, objetos con datos relacionales, y datos relacionales con XML son algunas de las tareas que LINQ simplificará. Uno de los aspectos clave de LINQ es que fue diseñado para ser usado contra cualquier tipo de objetos u origen de datos y para proveer un modelo de programación consistente para hacerlo. La sintaxis y los conceptos son los mismos entre todos sus usos. Una vez aprendido cómo usar LINQ sobre un arreglo o una colección, también se pueden conocer la mayoría de conceptos necesarios para aprovechar LINQ con una base de datos o con un archivo XML.
Otro aspecto importante de LINQ es que cuando lo usa, trabaja en un mundo fuertemente tipeado. Los beneficios incluyen una revisión en tiempo de compilación para las consultas, así como oportunas sugerencias de la característica IntelliSense de Visual Studio. LINQ cambiará significativamente algunos aspectos sobre cómo manejar y mani pular los datos con la aplicación y los componentes, ya que LINQ es un paso hacia un modelo de programación más declarativo. Tal vez se pregunte en el futuro no tan distante por qué tenía que escribir tantas líneas de código. Hay una dualidad en LINQ. Puede concebir a LINQ como dos partes complementarias: un conjunto de herramientas que trabajan con datos, y un conjunto de extensiones de lenguajes de programación.
1.2. LINQ como un conjunto de herramientas.
LINQ ofrece numerosas posibilidades y cambiará significativamente algunos aspectos de cómo se manejan y manipulan los datos datos con las aplicaciones y componentes. LINQ contiene varios proveedores, entre los cuales se tienen LINQ para Objetos, LINQ para SQL, y LINQ para XML. Los tres proveedores están construidos en la cima de la base común de LINQ. Estos proveedores conforman una colección de bloques integrados que incluyen operadores de consulta, expresiones de consulta, y árboles de expresiones, que le permite al juego de herramientas de LINQ ser extensible. Otras variantes de LINQ pueden ser creadas para proporcionar acceso a diversas clases de orígenes de datos. Las implementaciones de LINQ serán liberadas por los vendedores de software, y también se pueden crear implementaciones propias. Se puede conectar un amplio arreglo de orígenes de datos dentro de LINQ, incluyendo el sistema de d e archivos, directorio activo, VML, la bitácora de eventos de Windows W indows o cualquier otro origen de datos o API. Esto es bueno porque se puede obtener beneficio de las características de LINQ con varios de los orígenes de datos con los cuales cual es se trabaje cada día. De hecho, Microsoft ya ofrece más proveedores de LINQ que sólo LINQ para objetos, LINQ para SQL y LINQ para XML. La figura siguiente muestra cómo se representan los bloques construidos y el conjunto de herramientas en un diagrama.
Los proveedores LINQ presentados no son herramientas independientes, y pueden ser usados directamente en los lenguajes de programación. Esto es posible porque el marco de trabajo de LINQ viene como un conjunto de extensiones de lenguaje. Este es el segundo aspecto de LINQ.
1.3. LINQ como extensiones del lenguaje.
LINQ permite acceder información al escribir consultas sobre varios orígenes de datos y provee el mismo tipo de capacidades que ofrece SQL, pero en el lenguaje de programación de su elección, tal como se muestra a continuación:
El código anterior demuestra lo que se necesita escribir para extraer datos de una base de datos y crear un documento XML a partir de él. Imagine lo que haría para hacer lo mismo sin LINQ, y se dará cuenta cómo las cosas son más fáciles y más naturales con LINQ. Con las palabras reservadas from, where, orderby y select en el listado, es obvio que C# ha sido extendido para habilitar las consultas integradas en el lenguaje. En la figura siguiente se muestra una típica consulta integrada al lenguaje que es usada para hablar con los objetos, XML, o tablas de datos.
La consulta en la figura es expresada en C# y no en un nuevo lenguaje. LINQ no es un nuevo lenguaje. Está integrado en C# y VB .NET. Además, LINQ puede ser usado para evitar enredar su lenguaje de programación .NET con SQL, XML u otro lenguaje de datos específico. El conjunto de extensiones de lenguaje que viene con LINQ habilita las consultas sobre varios tipos de almacenes de datos para ser formulados justo en los lenguajes de programación. Piense en LINQ como un control universal remoto, si desea. A veces, lo usará para consultar una base de datos; en otros, consultará un documento XML. Pero hará todo esto en su lenguaje favorito, sin tener que cambiar a otro como SQL o XQuery.
2. ¿Para qué se necesita LINQ?
Se ha proporcionado un panorama general de LINQ. La gran pregunta en este punto es: ¿para qué se necesita una herramienta como LINQ? ¿Qué hace inconveniente a las he rramientas anteriores? ¿Fue LINQ creado sólo para trabajar con lenguajes de programación, datos relacionales y XML, de forma más conveniente?
En el inicio del proyecto LINQ era un hecho sencillo: la vasta mayoría de aplicaciones que eran desarrolladas accedían datos o hablaban con las bases de datos relacionales. Consecuentemente, para programar aplicaciones, aprender un lenguaje tal como C# no es suficiente. También tiene que aprender otro lenguaje como SQL, y la API que lo une con C# para formar la aplicación completa.
2.1. Problemas comunes.
El uso frecuente de bases de datos en aplicaciones requiere que el Framework de .NET trate sobre la necesidad de APIs que pueden acceder los datos almacenados en ellas. Por supuesto, esto ha sido el caso desde la primera aparición de .NET. La librería de clases del Framework de .NET (FCL) incluye ADO .NET, lo cual l e proporciona una API para acceder bases de datos relacionales y representar datos en memoria. Esta API consiste en clases tales como SqlConnection, SqlCommand, SqlReader, DataSet, y DataTable, por mencionar algunas. El problema con esas clases es que ellas obligan a los desarrolladores a trabajar explícitamente con las tablas, registros y columnas, mientras los lenguajes modernos como C# y VB .NET utilizan paradigmas orientados a objeto. Ahora que el paradigma orientado a objeto es el modelo dominante en el desarrollo de software, los desarrolladores incurren en una gran cantidad de sobrecarga en mapearlo a otras abstracciones, específicamente bases de datos relacionales y XML. El resultado es que mucho tiempo es gastado en escribir código de fontanería. El eliminar esta molestia incrementaría la productividad en la programación intensiva de datos, lo cual LINQ ayuda a hacer. Pero no es sólo la productividad, sino que además impacta en la calidad. Escribir código tedioso y código de plomería frágil puede conducir a defectos insidiosos en el software o a degradar el rendimiento. A continuación se muestra un acceso a datos típico en un programa .NET. Al observar los problemas que existen con el código tradicional, será capaz de ver cómo LINQ viene al rescate.
Con solo dar un vistazo a este código podemos enumerar varias limitaciones al modelo: A pesar de que se desea realizar una tarea simple, se deben realizar varios pasos y se requiere escribir mucho código. Las consultas son expresadas como cadenas citadas (1), lo que significa que evitan todo tipo de revisiones en tiempo de compilación. ¿Qué sucede si el código no contiene una consulta SQL válida? ¿Qué sucede si una columna ha sido renombrada en la base de datos? Lo mismo puede decirse de los parámetros (2) y de los conjuntos de resultados (3): están débilmente definidos. ¿Son las columnas del tipo de datos que se espera? También, ¿estamos seguros que estamos usando el número correcto de parámetros? ¿Están los nombres de los parámetros en sincronía entre la consulta y la declaración de parámetros? Las clases que usamos están dedicadas a SQL Server y no pueden ser usadas con otro servidor de bases de datos. Naturalmente, podemos usar DbConnection y sus derivados para evitar este tema, pero eso resolvería solamente la mitad del problema. El Verdadero problema es que SQL tiene tantos dialectos específicos a los proveedores y tipos de datos. El SQL que escribimos para un DBMS determinado es casi seguro que fallará para otro diferente. Existe otra solución. Podemos usar un generador de código de alguna de las varias herramientas de objetos mapeados relacionados disponibles. El problema es que esas herramientas tampoco son perfectas y tienen sus propias l imitaciones. Por ejemplo, si son diseñadas para acceder a una base de datos, la mayor parte del tiempo no maneja otros
orígenes de datos tales como documentos XML. También, u na cosa que los proveedores de lenguajes como Microsoft pueden hacer y que los vendedores de herramientas de mapeo no pueden es integrar el acceso a datos y las características de consulta justo en sus lenguajes. Las herramientas de mapeo a los sumo se presentan como una solución parcial del problema. La motivación para LINQ tiene doble propósito: Microsoft aún no tenía una solución de mapeo de datos, y con LINQ tenía la oportunidad de integrar consultas dentro de sus lenguajes de programación. Esto podía eliminar la mayoría de las limitaciones identificadas en el código anterior. La idea principal es que al usar LINQ se pueda ganar acceso a cualquier origen de datos al escribir consultas como la mostrada a continuación, directamente en el lenguaje de programación de uso diario.
En esta consulta, los datos pueden estar en memoria, en una base de datos, en un documento XML, o en cualquier otro lugar; la sintaxis permanecería similar, si no exactamente la misma. Este tipo de consultas puede ser usado con múltiples tipos de datos y diferentes orígenes de datos, gracias a las características de extensibilidad de LINQ. Por ejemplo, en el futuro posiblemente veamos una implementación de LINQ para consultar un subsistema de archivos o para llamar servicios web.
2.2. Tratando el paradigma del desajuste.
Continuemos revisando por qué es necesario LINQ. El hecho que los desarrolladores de aplicaciones modernas tengan que atender simultáneamente con lenguajes de programación de propósitos generales, datos relaciones, SQL, documentos XML, XPath, y así por el estilo, significa que se necesitan dos cosas: Ser capaz de trabajar con cualquiera de esas tecnologías o lenguajes individualmente. Mezclar y hacer coincidirlos para construir soluciones ricas y coherentes. El problema está en que la programación orientada a objetos, el modelo de bases de datos relacionales, y XML - por mencionar algunos - no fueron originalmente construidos para trabajar juntos. Representan diferentes paradigmas que no funcionan bien entre sí.
¿Qué es el desajuste de impedancia? Los datos son manipulados generalmente por software de aplicaciones escritas usando lenguajes de programación orientados a objetos tales como C#, VB .NET, Java, Delphi y C++. Pero traducir un objeto gráfico en otra representación, tal como tuplas de una base de datos relacional requiere a menudo de código tedioso. El problema general que LINQ trata ha sido expresado por Microsoft de esta manera: "Datos != Objetos". Más específicamente, con LINQ para SQL: "Datos Relacionales != Objetos". Los mismo aplica con LINQ para XML: "Datos XML != Objetos". Deberíamos agregar "Datos XML != Datos Relacionales". El término desajuste de impedancia se refiere a la incompatibilidad entre sistemas y describe una habilidad inadecuada de un sistema para acomodar los insumos provenientes de otro sistema. A pesar que el término se originó en el campo de la ingeniería eléctrica, ha sido generalizado y usado como un término técnico en análisis de sistemas, electrónica, física, ciencias de la computación e informática.
Mapeo de objetos relacionales Si se habla del paradigma orientado a objetos y el paradigma relacional, el desajuste existe en varios niveles. Veamos algunos. Las bases de datos relacionales y los lenguajes orientados a objetos no comparten el mismo conjunto de tipos de datos primitivos . Por ejemplo, las cadenas usualmente tienen una longitud delimitada en las bases de datos, lo cual no es el caso en C# o VB .NET, Esto puede ser un problema si intenta persistir una cadena de 150 caracteres en un campo de tabla que acepta solamente 100 caracteres. Otro ejemplo sencillo es que las mayorías de bases de datos no tienen un tipo de datos booleano, mientras que es frecuente el uso de valores verdadero/falso en muchos lenguajes de programación Las teorías de la programación orientada a objetos y relacionada tienen diferentes modelos de datos . Por razones de rendimiento y debido a su naturaleza intrínseca, las bases de datos relacionales son usualmente normalizadas . La normalización es un proceso que elimina la redundancia, organiza datos eficientemente, reduce el potencial de animalías durante las operaciones de datos y mejora la consistencia de datos. La normalización resulta en una organización de datos que es específica al modelo de datos relacional. Esto le impide un mapeo directo de tablas y registros a objetos y colecciones. Las bases de datos relacionales están normalizadas en tablas y relaciones, mientras los objetos usan herencia, composición, y gráficos de referencia compleja. Un problema común existe debido a que las bases de datos relacionales no tienen conceptos como herencia: mapear una jerarquía de clases a una base de datos relacional requiere el uso de "trucos". Los modelos de programación . En SQL se escriben consultas, y por tanto se tiene una forma declarativa y de mayor nivel para expresar el conjunto de datos sobre los que se tiene interés.
Con lenguajes de programación imperativos tales como C# o VB .NET, es necesario escribir ciclos for , sentencias if , y así por el estilo. Encapsulamiento . Los objetos son autocontenidos e incluyen tanto el comportamiento como los datos. En las bases de datos, los registros de datos no tienen comportamiento por sí mismos. Es posible actuar sobre registros de una base de datos sólo por medio del uso de consultas SQL o de procedimientos almacenados. En las bases de datos relacionales, el código y los datos están claramente separados.
El desajuste es el resultado de las diferencias entre el modelo de base de datos relacional y la típica jerarquía de clases orientadas a objetos. Se podría decir que las bases de datos relacionales son de Marte y los objetos son de Venus. Tomemos un ejemplo sencillo como el mostrado en la figura siguiente.
Se tiene un modelo de objetos que se desea mapear hacia un modelo relacional. Conceptos tales como herencia o composición no son soportados directamente por las bases de datos relacionales, lo cual significa que no se puede representar los datos de la misma forma en ambos modelos. Puede ver en el ejemplo que varios objetos y tipos de objetos pueden ser mapeados a una sola tabla. Aún si se deseara persistir un modelo de objetos como el que se presenta hacia una nueva base de datos relacional, no se podría usar mapeo directo. Por ejemplo, por razones de rendimiento y para evitar duplicidad, es mucho mejor en este caso crear sólo una tabla en la base de datos. Una consecuencia de hacer esto, sin embargo, es que los datos que vienen de la tabla en la base de datos no pueden ser fácilmente usados para repoblar un gráfico de objetos en memoria. Lo que se gana en un lado se pierde en el otro. Se debe ser capaz de diseñar esquemas de base de datos o un modelo de objetos para reducir el desajuste en ambos mundos, pero nunca se podrá eliminar debido a las diferencias
intrínsecas entre los dos paradigmas. A veces ni siquiera se tiene alternativa. A menudo, el esquema de bases de datos ya está definido, y en otras ocasiones se tiene que trabajar con objetos definidos por alguien más. El problema complejo de integrar orígenes de datos con programas incluye más que sólo leerlas de y escribirlas a una fuente de datos. Cuando la programación usa un lenguaje orientado a objetos, normalmente se desea que las aplicaciones usen un modelo de objetos que sea una representación conceptual del dominio del negocio, en lugar de estar atadas directamente a la estructura relacional. El problema es que en algún punto se necesita hacer que el modelo de objetos y el modelo relacional trabajen juntos. Esto no es una tarea fácil porque los lenguajes de programación orientados a objetos y .NET incluyen clases entidades, reglas de negocio, relaciones complejas y herencia, mientras que el origen de datos relacional incluye tablas, filas, columnas y llaves (primarias y s ecundarias). Una solución típica para unir lenguajes orientados a objetos y base de datos relacionales es el mapeo de objetos relacional . Esto se refiere al proceso de mapear el modelo de base de datos relacional al modelo de objetos, usualmente en ambos lados. Mapear puede ser definido como el acto de determinar cuántos objetos y sus relaciones son persistentes en almacenamiento de datos permanentes, en este caso bases de datos relacionales. Las bases de datos no se mapean de forma natural al modelo de objetos. Los mapeadores objeto-relacional son soluciones automatizadas para tratar el problema del desajuste. Para acortar la historia: se proporciona un mapeador objeto-relacional con clases, base de datos y configuraciones de mapeo, y el mapeador se encarga del resto. Genera consultas SQL, llena los objetos con datos de la base de datos, los persiste en la base de datos, y así por el estilo. Como puede imaginar, ninguna solución es perfecta, y los mapeadores objeto-relacional podrían ser mejorados. Algunas de sus principales limitaciones son: Se requiere un buen conocimiento de herramientas antes de poder usarlos eficientemente y evitar problemas de desempeño. El uso óptimo todavía requiere conocimiento de cómo trabajar con una base de datos relacional. Las herramientas de mapeo no siempre son tan eficientes como el código escrito a mano. No todas las herramientas vienen con soporte para validación en tiempo de compilación. Múltiples herramientas de mapeo objeto-relacional están disponibles para .NET. Hay una selección de productos de código libre, gratuitos y comerciales.
Mapeo de objetos XML.
Análogo a la impedancia de desajuste objeto-relacional, un desajuste similar también existe entre objetos y XML. Por ejemplo, el tipo sistema descrito en la especificación de esquemas
W3C y XML no tiene relación de uno a uno con el tipo sistema del marco de trabajo .NET. Sin embargo, usar XML en una aplicación .NET no es mayor problema porque ya se tienen APIs que tratan con esto bajo el espacio de nombres System.Xml, al igual que soporte integrado para serialización y deserialización de objetos. Sin embargo, mucho código tedioso es requerido la mayoría de las veces para hacer aún cosas sencillas sobre documentos XML. Dado que XML se ha vuelto tan penetrante en el mundo del software moderno, algo se tenía que hacer para reducir el trabajo requerido para tratar con XML en los lenguajes de programación. Cuando se mira en esos dominios, es destacable cuánta diferencia existe. El origen principal de contención relaciona los siguientes hechos: Las bases de datos relacionales están basadas en el álgebra relacional y son todas sobre tablas, filas, columnas y SQL. XML es sobre documentos, elementos, atributos, estructuras de jerarquía, y XPath. Los lenguajes de programación de propósitos generales orientados a objetos y .NET viven en un mundo de clases, métodos, propiedades, herencia y ciclos. Muchos conceptos son específicos a cada dominio y no tienen mapeo directo hacia el otro dominio. La figura siguiente proporciona un panorama general de los conceptos usados en .NET y la programación orientada a objetos, en comparación con los conceptos usados en las fuentes de datos tales como XML o bases de datos relacionales. Demasiado a menudo, los programadores tienen que hacer un poco de trabajo de albañilería para unir estos dominios diferentes. Diferentes APIs para cada tipo de datos cau só que los desarrolladores invirtieran una exagerada cantidad de tiempo aprendiendo cómo escribir, depurar y reescribir código frágil.
Los culpables tradicionales que arruinan la lógica son malas cadenas de consultas SQL o etiquetas XML, o contenido que no se revisa hasta el tiempo de ejecución. Los lenguajes .NET tales como C# y VB .NET ayudan a los desarrolladores y proporcionan recursos tales como IntelliSense, código fuertemente tipeado, y revisiones en tiempo d e compilación. Sin embargo, esto puede arruinarse si se empieza a incluir consultas SQL mal formadas o fragmentos de XML en el código, ninguno de los cuales es validado por el compilador. Una solución exitosa requiere unir las diferentes tecnologías y solucionar el desajuste de impedancia objeto-persistencia, un problema que es todo un reto y consume muchos recursos. Para resolver este problema, se debe resolver los siguientes puntos entre .NET y los elementos de origen de datos: Fundamentalmente diferentes tecnologías. Diferentes cojuntos de habilidades. Diferentes personas y propietarios para cada una de las tecnologías. Diferentes modelos y principios de diseño. Algunos esfuerzos han sido hechos para reducir el desajuste de impedancia al unir algunas piezas de un mundo en otro. Por ejemplo, SQLXML 4.0 une SQL con XSD; System.Xml abarca XML/XML DOM/XSL/XPath y CLR; la API ADO .NET une SQL y los tipos de datos CLR; y SQL Server 2005 incluye integración con CLR. Todos estos esfuerzos son prueba que la integración de datos es esencial; sin embargo, representan movimientos diferentes sin una base común, lo cual los hace difíciles de usar juntos. LINQ, por el contrario, ofrece una infraestructura común para tratar los desajustes de impedancia.
2.3. LINQ al rescate.
Para tener éxito en el uso de objetos y bases de datos relacionales juntos, es necesario entender ambos paradigmas, junto con sus diferencias, y entonces efectuar concesiones inteligentes basadas en ese conocimiento. El objetivo principal de LINQ para SQL es liberarse de, o al menos reducir, la necesidad de preocuparse por esas limitaciones. Un desajuste de impedancia obliga a escoger un lado o el otro como el lado "primario". Con LINQ, Microsoft selecciona el lado del lenguaje del programador, debido a que es más fácil adaptar los lenguajes C# y VB .NET que cambiar SQL o XML, el objetivo es hacia las capacidades profundamente integradas de consultas de datos y de manipulación de lenguajes dentro de los lenguajes de programación. LINQ quita muchas de las barreras entre objetos, bases de datos y XML. Permite trabajar con cada uno de estos paradigmas usando los mismos recursos integrados en el sistema. Por ejemplo, se puede trabajar con datos XML y datos provenientes de una base de datos relacional dentro de la misma consulta. Debido a que el código vale más que mil palabras, a continuación se presenta una muestra sencilla del poder de LINQ para recuperar datos de una base de datos y crear un documento
XML en una sola consulta. El código siguiente crea una alimentación RSS basada en datos relacionales.
Observe cómo LINQ facilita trabajar con datos relacionales y XML en la misma porción de código. Si ya ha efectuado este tipo de trabajos antes, debería ser obvio que este código es muy legible y conciso en comparación con las soluciones disponibles antes del aparecimiento de LINQ.
3. Objetivos de diseño y orígenes de LINQ.
Es importante conocer claramente lo que pretendía lograr Microsoft con LINQ. Por esta razón se revisarán los objetivos de diseño del proyecto LINQ. También es interesante saber de donde toma sus raíces LINQ y entender los vínculos con otros proyectos de los cuales habrá escuchado. LINQ no es un proyecto reciente de Microsoft en el sentido que hereda muchas características de trabajo de investigación y desarrollo hecho sobre los últimos años.
3.1. Los objetivos del proyecto LINQ.
La tabla siguiente muestra los objetivos de diseño que Microsoft definió para el proyecto LINQ, con el objeto de proporcionar un entendimiento claro de lo que ofrece LINQ.
Objetivo
Motivación
Sintaxis de consulta unificada entre orígenes de datos para evitar diferentes lenguajes de diferentes orígenes de datos. Integrar objetos, datos relacionales y XML.
Potencia similar a SQL y XQuery en C# y VB .NET.
Un solo modelo para procesar todos los tipos de datos, sin importar el origen o la representación en memoria. Integrar habilidades de consulta justo dentro de los lenguajes de programación.
Modelo de extensibilidad para Habilitar la implementación de otros lenguajes de lenguajes. programación.
Modelo de extensibilidad para múltiples orígenes de datos.
Seguridad de tipo.
Ser capaz de acceder otros orígenes de datos, además de las bases de datos relacionales o documentos XML. Permitir a otros marcos de trabajo habilitar el soporte a LINQ para sus propias necesidades. Revisión de tipo en tiempo de compilación para evitar problemas que antes eran descubiertos sólo en tiempo de ejecución. El compilador atrapará errores en sus consultas.
Soporte extensivo de IntelliSense (habilitado por un fuerte tipeado).
Asistir a los desarrolladores cuando escriben consultas para mejorar la productividad y para ayudarles a elevar la velocidad con la nueva sintaxis. El editor lo guiará cuando escriba consultas.
Soporte de depurador.
Permite a los desarrolladores depurar consultas LINQ paso a paso y con abundante información de depuración.
Construido sobre los fundamentos de C# a.0 y 2.0, VB .NET 7.0 y 8.0.
Reutiliza las ricas características que han sido implementadas en las versiones previas de los lenguajes.
Corre sobre CLR .NET 2.0.
Evita la necesidad de un nuevo tiempo de ejecución y molestias innecesarias de instalación.
Ser 100% compatible con versiones anteriores.
Ser capaz de usar colecciones estándar y genéricas, enlace de datos, controles web y Windows existentes, etc.
La primera característica presentada en la tabla anterior es la habilidad de tratar con varios tipos y orígenes de datos. LINQ se empaca con implementaciones que soportan consultas sobre colecciones de objetos regulares, bases de datos, entidades y orígenes XML. Debido a que LINQ soporta una rica extensibilidad, los desarrolladores pueden también integrarlo fácilmente con otros orígenes de datos y proveedores. Otra característica esencial de LINQ consiste en ser fuertemente tipeado. E sto significa lo siguiente:
Se obtiene revisión en tiempo de compilación para todas las consultas. A diferencia de las sentencias SQL actuales, donde típicamente sólo se encuentra que algo está mal hasta el tiempo de ejecución, se puede revisar durante el desarrollo que el código esté correcto. El beneficio directo es una reducción del número de problemas descubiertos más tarde en producción. La mayoría de las veces, estos temas son debido a factores humanos. Las consultas fuertemente tipeadas permiten detectar errores cometidos por el desarrollador a cargo de la digitación. Se obtiene IntelliSense dentro de Visual Studio cuando se escriben consultas LINQ. Esto no sólo hace más fácil la escritura de código, sino que también hace más fácil trabajar sobre colecciones simples y complejas, así como modelos de objetos orígenes de datos.
3.2. Un poco de historia.
LINQ es el resultado de un largo proceso de investigación dentro de Microsoft. Varios proyectos que involucran evolución de lenguajes de programación y métodos de acceso a datos pueden ser considerados los padres de LINQ para Objetos, LINQ para XML (anteriormente conocido como XLinq), y LINQ para SQL (an teriormente conocido como DLinq).
Cω (o el lenguaje C -Omega). Cω (pronunciado "c-omega") fue un proyecto de Microsoft Research que extiende el lenguaje en varias áreas, sobresaliendo las siguientes:
Una extensión de flujo de control para concurrencia de área amplia asíncrona (anteriormente conocido como C# Polifónico). Una extensión de tipo de datos para manipulación de XML y base de datos (anteriormente conocido como Xen y X#).
Cω cubrió más de lo que viene con LINQ, pero gran parte de lo que ahora es incluido como parte de la tecnología LINQ ahora está presente en Cω. El proyecto Cω fue concebido para experimentar con consultas integradas, mezclando C# y SQL, C# y XQuery, etc. Esto fue realizado por investigadores como Erik Meijer, Wolfram Schulte y Gavin Bierman, quienes publicaron varios trabajos sobre el tema. Cω fue liberado como un avance en 2004. Mucho ha sido aprendido desde ese prototipo, y pocos meses después, Anders Hejlsberg, jefe de diseño del lenguaje C#, anunció que Microsoft estaría trabajando en la aplicación de ese conocimiento en C# y en otros lenguajes de programación. Anders dijo en ese momento que su interés particular en los últimos dos años había sido pensar profundamente sobre el gran desajuste de impedancia entre los lenguajes de programación - C# en particular - y el mundo de los datos. Esto incluye bases de datos y SQL, pero también XML y XQuery, por ejemplo. Las extensiones de Cω hacía el sistema tipo .NET y hacia el lenguaje C# fueron los primeros pasos hacia un sistema unificado que tratara las consultas estilo SQL, consultas de conju ntos de resultados, y contenido XML como miembros de pleno derecho del lenguaje. Cω introduce el tipo stream, el cual es análogo al tipo System,Collections.Generic de .NET Framework 2.0. Cω también define constructores para tuplas tipeadas (llamadas anonymus structs ), los cuales son similares a los tipos anónimos que se tienen en C# 3.0 y VB .NET 9.0. Otra cosa que soportaba Cω es XML embebido, algo que se pudo ver en VB .NET 9.0 (pero no en C# 3.0).
Object Spaces
LINQ para SQL no es el primer intento de Microsoft para un mapeo de objetos-relaciones. Otro proyecto con una fuerte relación con LINQ fue Object Spaces. El primer avance del proyecto Object Spaces apareció en la presentación de ADO .NET PDC 2001. Object Spaces fue un conjunto de APIs de acceso a datos. Permitían que los datos fueran tratados como objetos, independiente de los datos subyacentes almacenados. Object Spaces también introdujo OPath, un lenguaje de consultas de objeto propietario. En 2004, Microsoft anunció que Object Spaces dependía del proyecto WinFS (un proyecto para sistemas de archivos relacionados que Microsoft había estado desarrollando para Windows. Fue cancelado en 2006), y por tanto sería pospuesto en el plazo Orcas (el siguiente lanzamiento después de .NET 2.0 y Visual Studio 2005). Ningún nuevo lanzamiento se dio después de eso. Todos se dieron cuenta que Object Spaces nunca vería la luz del día cuando Microsoft anunció que WinFS no sería incluido en la primera liberación de Windows Vista.
Implementación XQuery.
Similar a lo que sucedió con Object Spaces y por el mismo tiempo, Microsoft había empezado a trabajar en un procesador XQuery. Un avance fue incluido en la primera liberación beta de la
versión 2.0 de .NET Framework, pero eventualmente se de cidió no incluir una implementación XQuery del lado del cliente en la versión final. Un problema con XQuery es que fue un lenguaje adicional que tendríamos que aprender expecíficamente para tratar con XML. ¿Por qué todos estos pasos hacia atrás? ¿Por qué Microsoft aparentemente detuvo el trabajo sobre esas tecnologías? La causa saltó a la vista en PDC 2005, cuando el proyecto LINQ fue anunciado. LINQ había sido diseñado por Anders Hejlsberg y otros en Microsoft para tratar el problema del desajuste de impedancia desde dentro de los lenguajes de programación tales como C# y VB .NET. Con LINQ se puede consultar casi todo. Esto es porque Microsoft favoreció LINQ en lugar de continuar invirtiendo en proyectos separados como Object Spaces o soporte pa ra XQuery en el lado del cliente. Como habrá visto, LINQ tiene una rica historia detrás de él, y se ha beneficiado de todos los trabajos de investigación y desarrollo de los proyectos previos y ahora extintos.
5.2. Conociendo los patrones de acceso a datos de LINQ Unidad V - Acceso a datos con LINQ. 5.2. Conociendo los patrones de acceso a datos de LINQ.
Objetivo General. Usar la herramienta de acceso a datos de LINQ en conjunto con el lenguaje de desarrollo para hacer consultas directas a la base de datos y la administración centralizada de datos.
Objetivo Específico. Identificar los patrones exactos para la manipulación de datos en los casos de consultas complejas y operaciones de mantenimiento.
1. LINQ para Objetos: consultando colecciones en memoria.
Antes de iniciar, es necesario revisar todo lo que se necesita para probar el código. A continuación se presentan los requerimientos que se deben satisfacer.
1.1. Soporte para el compilador y .NET Framework, y software requerido.
LINQ es entregado como parte de la ola Orcas, lo cual incluye Visual Studio 2008 y .NET Framework 3.5. Esta versión del marco de trabajo viene con librerías adicionales y actualizadas, así como nuevos compiladores para los l enguajes C# y VB .NET, pero permanecen compatibles con .NET Framework 2.0. Las características de LINQ son asuntos del compilador y de las librerías, no del runtime. Es importante entender que a pesar que los lenguajes C# y VB .NET han sido enriquecidos y algunas nuevas librerías han sido agregadas al .NET Framework, el runtime de .NET (el CLR) no necesitó evolucionar. Nuevos compiladores fueron ne cesarios para C# 3.0 y VB .NET 9.0, pero el runtime requerido aún es la versión 2.0 no modificada. Esto significa que las aplicaciones que haya construido usando LINQ pueden correr en un runtime .NET 2.0 (sin embargo, .NET 2.0 Service Pack 1 es necesario para que funcione LINQ para SQL). LINQ y LINQ para XML, o al menos un subconjunto de ellos, son soportados por las liberaciones actuales del runtime Silverlight. Están disponibles en los espacios de nombre System,Linq y System.Xml.Linq. Para configurar la computadora y poder ejecutar el código, se requiere instalar lo siguiente: Al menos una de las siguientes versiones de Visual Studio: Visual C# 2008 Express Edition. Visual Basic 2008 Express Edition. Visual Web Developer 2008 Express Edition. Visual Studio 2008 Standard Edition o superior. Si se quiere ejecutar LINQ para SQL, se requiere uno de los siguientes:
SQL Server 2005 Express Edition o SQL Server 2005 Compact Edition (incluido con la mayoría de versiones de Visual Studio). SQL Server 2005. SQL Server 2000a. Una versión posterior de SQL Server (los nuevos tipos de datos proporcionados por SQL Server 2008 no son soportados en la primera liberación de LINQ para SQL).
1.2. Consideraciones de lenguaje.
Se asume que tiene conocimiento del lenguaje de programación C#. Por cuestiones de simplicidad, se explicará el código a medida que se vaya presentando. A continuación se muestra el código LINQ para Objetos:
Si se compila y ejecuta este código, la salida sería: Hola Linq Carro Mundo Como puede evidenciarse a partir de los resultados, se ha filtrado un listado de palabras para seleccionar sólo aquellas cuya longitud sea igual o menor a cinco caracteres. Se podía argumentar que el mismo resultado podría ser logrado sin el uso de LINQ con el código que se muestra a continuación:
Observe que el "código antiguo" es mucho más corto que la versión LINQ y muy fácil de leer. Bien, no se rinda aún. Hay mucho más de LINQ que lo mostrado en este primer ejemplo sencillo. Ahora se mejorará el ejemplo con agrupamiento y ordenamiento. Esto debería dar una idea sobre por qué LINQ es útil y poderoso. Para la consulta siguiente, se espera obtener el resultado siguiente: Palabras de longitud 11 Maravilloso Palabras de longitud 5 Carro Mundo Palabras de longitud 4 Hola Linq Se puede usar el siguiente código C# mostrado en el listado siguiente:
En los ejemplos anteriores se ha expresado en una consulta (o más exactamente en dos consultas anidadas) lo que podría formularse como "palabras de un listado ordenadas alfabéticamente y agrupadas por su longitud en orden descendente". Como ejercicio, se le invita a que trate de escribir el mismo código sin usar LINQ. Si se toma el tiempo para hacerlo, verá que toma más código y requiere manejar muchas colecciones. Una de las primeras ventajas de LINQ que resalta de este ejemplo es la expresividad que habilita: se puede expresar declarativamente lo que se desea lograr usando consultas en lugar de escribir complicados fragmentos de código. Si está familiarizado con SQL, probablemente ya tiene una buena idea de lo que el código está haciendo. Además de todas estas consultas similares a SQL, LINQ también proporciona varias otras funciones tales como Sum, Min, Max, Average y muchas más que permiten realizar un rico conjunto de operaciones. Por ejemplo, aquí se suma la cantidad de cada orden en una lista de órdenes para calcular la cantidad total:
decimal cantidadTotal = orders.Sum(order => order.Amount);
2. LINQ para XML: consultando documentos XML.
La extensibilidad de la arquitectura de consultas de LINQ es usada para proporcionar implementaciones que trabajan sobre datos XML y SQL. LINQ para XML aprovecha el marco de trabajo de LINQ para ofrecer consultas XML y transforma capacidades integradas en el lenguaje de programación .NET anfitrión. También puede pensar en LINQ para XML como una API XML con todas las funciones comparable al System.Xml .NET 2.0 rediseñado y modernizado, más algunas cara cterísticas de XPath y XSLT. LINQ para XML proporciona facilidades para editar documentos XML y elementos de árboles en memoria, así como facilidades de flujo. Esto significa que será capaz de usar LINQ para XML con el propósito de desarrollar más fácilmente muchas de las tareas de procesamiento de XML que ha estado desarrollando con las API XML tradicionales del espacio de nombres System.Xml. Primero se examinará por qué es necesaria una API XML como LINQ para XML comparándola con otras alternativas. Luego se efectuarán los primeros pasos con un poco de código usando LINQ para XML en un ejemplo sencillo.
2.1. ¿Por qué se necesita LINQ para XML?
Hoy en día, XML es omnipresente, y es usado extensivamente en aplicaciones escritas usando lenguajes de propósito general tales como C# o VB .NET. Es usado para intercambiar datos entre aplicaciones, almacenar información de configuración, persistir datos temporales, generar páginas o reportes web, y hacer muchas otras cosas. Hasta ahora, XML no ha sido soportado de forma nativa por la mayoría de lenguajes de programación, por lo cual se requiere el uso de APIs para manejar datos XML. Estas APIs incluían XmlDocument, XmlReader, XPathNavigator, XslTransform for XSLT, SAX e implementaciones XQuery. El problema es que estas APIs no están bien integradas con los lenguajes de programación, requiriendo a menudo de varias líneas de código complejo innecesario para lograr un simple resultado. LINQ para XML extiende la característica de consulta de lenguaje integrado ofrecida por LINQ para agregar soporte para XML. Ofrece el poder expresivo de XPath y XQuery pero en C# o VB .NET, con la seguridad de tipo e IntelliSense. Si ha trabajado con documentos XML con .NET, probablemente usa XML DOM (Document Object Model) disponible en el espacio de nombres System.Xml. LINQ para XML supera la experiencia con el DOM para mejorar el conjunto de herramientas del desarrollador y evitar las limitaciones del DOM.
La tabla siguiente compara las características de LINQ para XML con las de XML DOM.
Características de LINQ para XML
Características de XML DOM
Centrado en elemento.
Centrado en documento.
Modelo declarativo.
Modelo imperativo.
El código de LINQ para XML presenta una distribución similar a la estructura jerárquica de un documento XML.
Sin similitudes entre el código y la estructura del documento.
Consultas integradas al lenguaje.
Sin integración de consultas.
La creación de elementos y atributos puede ser realizada en una instrucción; los nodos de texto sólo son cadenas.
Las cosas básicas requieren mucho código.
Soporte de espacio de nombre XML simplificado.
Requiere el tratamiento con prefijos y "administradores de espacio de nombres".
Más rápido y más pequeño.
Pesado y de uso intensivo de memoria.
Capacidades de flujo.
Todo es cargado en memoria.
APIs de simetría en elementos y atributos.
Diferentes maneras de trabajar con varios fragmentos de documentos XML.
Mientras el DOM es de bajo nivel y requiere mucho código para formular adecuadamente lo que se desea lograr, LINQ para XML proporciona una sintaxis de alto nivel que permite realizar cosas sencillas de manera fácil. LINQ para XML también permite un enfoque centrado en elemento, en comparación con el enfoque centrado en documento de DOM. Esto significa que puede trabajar fácilmente con fragmentos XML (elementos y atributos) sin tener que crear un documento XML completo. Dos clases que .NET Framework ofrece son XmlReader y XmlWriter. Estas clases proporcionan soporte para trabajar con texto XML en su forma original y son de más bajo nivel que LINQ para XML. LINQ para XML usa las clases XmlReader y XmlWriter de manera subyacente y no es una API XML completamente nueva. Una ventaja de esto es que permite que LINQ para XML permanezca compatible con XmlReader y XmlWriter. LINQ para XML hace más directa la creación de documentos, pero también hace más fácil consultar documentos XML. Expresar consultas sobre documentos XML se siente más natural
que tener que escribir mucho código con varios ciclos de instrucciones. También, siendo parte de la familia de tecnologías LINQ, es una buena decisión cuando se necesita unir diversidad de orígenes de datos. Con LINQ para XML, Microsoft está apuntando al 80% de los casos de uso. Esos casos involucran formatos XML directos y procesamiento común. Para el resto de los casos, los desarrolladores continuarán usando las otras APIs. También, a pesar que LINQ para XML toma inspiración de XSLT, XPath y XQuery, esas tecnologías tienen beneficios por sí mismas y están diseñadas para casos de uso específicos, y dentro de esos alcances LINQ para XML no es capaz en ninguna forma de competir con ellos. LINQ para XML no es suficiente para algunos casos específicos, pero su compatibilidad con otras APIs XML permiten usarlo en combinación con esas APIs.
2.2. Código LINQ para XML.
El ejemplo siguiente trabaja con libros. Para este caso particular, se desea filtrar y guardar un conjunto de objetos Book como XML. Así debería ser definida la clase Book en C#:
Supongamos que ahora tenemos la siguiente colección de libro
Usando LINQ para XML, esto puede ser hecho con el código siguiente:
En contraste, el código siguiente muestra cómo se debería construir el mismo documento sin LINQ para XML, usando XML DOM:
Como puede apreciarse, LINQ para XML es más visual que el DOM. La estructura del código para obtener el fragmento XML es cercano al documento que se desea producir. Se podría decir que es un código WYSIWYM: lo que se mira es lo que se desea expresar (What You See Is What You Mean). Microsoft llama a este enfoque el patrón de Construcción Funcional . Permite estructurar código de forma tal que refleja la forma de un documento XML (o un fragmento) que se está construyendo.
3. LINQ para SQL: Consultando bases de datos relacionales.
La ambición de LINQ es hacer de las consultas una parte natural del lenguaje de programación. LINQ para SQL, que hizo su aparición como DLinq, aplica este concepto para permitir a los desarrolladores consultar bases de datos relacionales usando la misma sintaxis que ha visto en LINQ para Objetos y LINQ para XML.
3.1. Vista previa de las características de LINQ para SQL.
LINQ para SQL proporciona acceso a datos integrado al lenguaje usando mecanismos de extensión de LINQ. Se construye sobre ADO .NET para mapear tablas y filas a clases y objetos. LINQ para SQL usa información de mapeado codifica en los atributos personalizados de .NET o contenidos en un documento XML. Esta información es usada para manejar automáticamente la persistencia de objetos en bases de datos relacionales. Una tabla puede ser mapeada a una clase y las columnas de una tabla a las propiedades de la clase, y las relaciones entre tablas pueden ser representadas por propiedades adicionales. LINQ para SQL automáticamente mantiene registro de los cambios a objetos y actualiza la base de datos consecuentemente por medio de consultas dinámicas SQL o procedimientos almacenados. Esta es la razón por la cual no es necesario proporcionar las consultas SQL por nosotros la mayoría de las veces.
3.2. LINQ para SQL.
El siguiente código filtra una colección de contactos en memoria en base a la ciudad.
Esta consulta trabaja sobre una lista de contactos de una base de datos. Observe cuán sutil es la differencia entre las dos consultas. Sólo el objeto sobre el cual se está trabajando es diferente; la sintaxis de la consulta es exactamente la misma. Esto muestra cómo se p odrá trabajar de la misma forma con múltiples tipos de datos. Usted sabe que el lenguaje de datos que una base de datos relacional entiende es SQL, y sospecha que la consulta LINQ debería ser trasladada a SQL en el mismo punto. Este es el corazón de la tecnología: en el primer ejemplo, la colección es iterada en memoria, mientras que en el segundo código, la consulta es usada para generar una consulta SQL que es enviada al servidor de base de datos. En el caso de consultas LINQ para SQL, el procesamiento real sucede en el servidor de base de datos. Lo sorprendente de estas
consultas es que se tiene una consulta API fuertemente tipeada, en contraste con SQL, donde las consultas son expresadas en cadenas y no validadas en tiempo de compilación.
Clases entidad. El primer paso en construir una aplicación LINQ para SQL es declarar las clases que se usarán para representar los datos de la aplicación: las entidades. En el ejemplo sencillo, se definirá una clase llamada Contact y asociada con la tabla Contacts de la base de datos de muestra Northwind proporcionada por Microsoft. Para hacer esto sólo se aplica un atributo personalizado a la clase:
El atributo Table es proporcionado por LINQ para SQL en el espacio de nombres System.Data.Linq.Mapping. Tiene una propiedad Name que es usada para especificar el nombre de la base de datos. Además de asociar las clases entidad con las tablas, es necesario definir cada campo o propiedad que se pretende asociar con una columna en la tabla. Esto se hace con el atributo Column:
El atributo Column también es parte del espacio de nombres System.Data.Linq.Mapping. Tiene una variedad de propiedades que se pueden usar para personalizar el mapeo exacto entre los campos o propiedades y las columnas de la base de datos. Puede ver que se ha usado la propiedad IsPrimaryKey para decirle a LINQ que la columna de la tabla llamada ContactID es parte de la llave primaria de la tabla. Observe cómo se indica que la columna ContactName será mapeada al campo Name. No se especifican los nombres de las demás columnas o de los tipos de las columnas: en nuestro caso, LINQ para SQL las deducirá de los campos a partir de la clase.
El DataContext. Lo siguiente que se necesita preparar antes de poder usar las consultas integradas al lenguaje es un objeto System.Data.Linq.DataContext. El propósito del DataContext es traducir las solicitudes de los objetos en consultas SQL hechas sobre la base de datos y luego ensamblar objetos a partir de los resultados. Como se mencionó antes, se usará la base de datos Northwnd.mdf proporcionada por Microsoft. La creación del objeto DataContext se realiza de la manera siguiente:
El constructor de la clase DataContext toma una cadena de conexión como parámetro. El DataContext proporciona acceso a las tablas en la base de datos. Aquí es como se obtiene acceso a la tabla Contracts mapeada a la clase Contact :
DataContext.GetTable es un método genérico que permite trabajar co n objetos fuertemente tipeados. Esto es lo que permitirá usar una consulta LINQ. A continuación se presenta el código completo del ejemplo:
Aquí está la consulta SQL que fue enviada al servidor de forma transparente:
Observe lo fácil que es obtener acceso fuertemente tipeado a una base de datos gracias a LINQ. Este es un ejemplo simplista, pero da una buena idea de lo que LINQ para SQL tiene que ofrecer y cómo podría ser cambiada la forma de trabajar con bases de datos. A continuación se muestra un resumen de lo que LINQ para SQL ha hecho automáticamente: Abrir una conexión a la base de datos. Generar la consulta SQL. Ejecutar la consulta SQL sobre la base de datos. Crear y llenar los objetos a partir de los resultados tabulares. Como ejercicio, puede intentar hacer lo mismo sin LINQ para SQL. Por ejemplo, puede tratar de usar un DataReader. Observará las siguientes características del código antiguo cuando se compara con el código LINQ para SQL:
Consultas SQL escritas explícitamente entre comillas. No hay revisión en tiempo de compilación. Parámetros débilmente relacionados. Conjunto de resultados tipeados débilmente relacionados. Se requiere más código. Se requiere más conocimiento. Escribir código estándar de acceso de acceso a datos dificulta la productividad para cada caso sencillo. En contraste, LINQ para SQL permite escribir código de acceso a datos que no se interpone en el camino.
3.3. Una revisión más detallada a LINQ para SQL.
Se ha visto que LINQ para SQL es capaz de generar consultas SQL dinámicas basadas en consultas integradas al lenguaje. Esto no se adapta para cada situación, por lo que LINQ para SQL también soporta consultas SQL personalizadas y procedimientos almacenados, por lo
que podemos usar nuestro código escrito por nosotros mismos y aún beneficiarnos de la infraestructura LINQ para SQL. En el ejemplo se proporcionó información de mapeo usando atributos personalizados en las clases; pero si se prefiere no tener este tipo de información fuertemente codificada, es libre de usar un archivo XML externo mapeado para hacer lo mismo. Para obtener un mejor entendimiento de cómo LINQ para SQL trabaja, se crearon las entidades de clases y se proporcionó la información de mapeo. En la práctica, este código típicamente sería generado por las herramientas que vienen con LINQ para SQL o usando el diseñador gráfico de LINQ para SQL. La lista de características de LINQ para SQL es mucho más larga que esto e incluye cosas como un soporte para enlace de datos, interoperabilidad con ADO .NET, administración de l a concurrencia, soporte para herencia, y ayuda para depuración.
5.3. Desarrollando un mantenimiento de datos con LINQ Unidad V - Acceso a datos con LINQ. 5.3. - Desarrollando un mantenimiento de datos con LINQ.
Objetivo General. Usar la herramienta de acceso a datos de LINQ en conjunto con el lenguaje de desarrollo para hacer consultas directas a la base de datos y la administración centralizda de datos.
Objetivo Específico. Crear un mantenimiento de datos usando los patrones de LINQ.
1. Iniciando con LINQ para SQL.
LINQ para SQL permite reducir la carga de red y el procesamiento del cliente, tomando ventaja de los índices de las bases de datos.
Se tomará como ejemplo la consulta a una tabla llamada books para consultar los libros que cuestan menos de $30 y aruparlos por tema. Para hacer esto, se pueden separar los procesos en varias tareas: seleccionar los temas ordenados, sel eccionar el correspondiente filtro sobre el precio, combinar los temas con los libros, y proyectar sólo los resultados que se necesitan. Se iniciará con las tareas relacionadas a los libros y luego se tratará la unión con los temas.
En este punto aún se requiere solicitar toda la base de datos y filtrarlo en el cliente. Para probar esto, a continuación se presenta la sentencia SQL enviada a la base de datos:
Aún se están recuperando todos los campos de la base de datos, a pesar de que solamente se usarán dos de ellos. Adicionalmente, se están recuperando todos los registros de la base de datos, no sólo los que cumplen el criterio. Además, tampoco se están aprovechando los índices debido a que se ordenan los resultados en el cliente. Idealmente, se debería definir una sentencia similar a la siguiente:
¿Cuántos cambios se necesitan para acomodar la consulta? ¡Ninguno! Todo lo que se necesita es modificar la clase Book y cambiar cómo se accede a ella. Revisemos el objeto Book y la tabla para ver qué cambios serán necesarios.
Para empezar, se aplicará un mapeado de campos entre la tabla y el objeto destino. Después se verá la unión de esta tabla con los temas correspondientes y se verá cómo se manejan las relaciones de llaves foráneas en la base de datos. Por ahora, se limitará el alcance a una sola tabla. Empecemos mirando el código de la clase Book , tal como se muestra a continuación:
El código anterior utiliza propiedades autoimplementadas. En este punto aún se necesita una manera de llenar los objetos con datos provenientes de la base de datos. Se hace esto configurando una serie de mapeos para especificar cómo los objetos se relacionan con las tablas y columnas de la base de datos.
1.1. Estableciendo el mapeo de objetos.
Empecemos la exploración de LINQ para SQL habilitando la clase Book . Para iniciar, agregar una referencia al ensamblado System.Data.Linq , el cual es parte del Framework .NET 3.5, y agregue una sentencia using en la parte superior de la clase. El espacio de nombres Mapping contiene atributos que nos permiten establecer declarativamente las relaciones entre la base de datos y los objetos. using System.Data.Linq.Mapping; Se usarán atributos para declarar los mapeos de datos. En la mayoría de los casos se necesita identificar dos cosas en una clase: con qué tablas está relacionado y a cuáles columnas están mapeados los valores. El mapear la tabla book con el objeto es quizás el mapeo más sencillo. En este caso, la base de datos tiene una tabla llamada book , por lo que se tiene un mapeo uno a uno entre ambos objetos, y tienen el mismo nombre. Para declarar el mapeado, se agrega un atributo a la declaración de clase llamado Table, como se muestra:
Si se desea ser más explícito, se puede declarar el nombre de la tabla origen usando un parámetro con nombre, Name, como se muestra:
Ahora que se ha mapeado la clase a la tabla, se necesita indicar cuáles propiedades son almacenadas como columnas en la tabla y cómo las columnas mapean hacia la información de las propiedades. Se hace esto agregando un atributo Column a las propiedades que se desea mapear. Por ejemplo, para mapear la propiedad Title a la columna Title de la tabla Book , se agrega un atributo Column antes de la declaración de propiedad:
No estamos limitados a los mapeos directos. Podemos especificar alguna traducción entre el nombre de la columna de tabla y el nombre de la propiedad del objeto. Por ejemplo, la tabla Book tiene una columna llamada PubDate . Para hacerle el trabajo más fácil al
desarrollador de la aplicación cliente que trabaja con el objeto de negocios, sería deseable usar una convención de nombres más clara y nombrar a la propiedad como PublicationDate . Para hacer esto, se especifica el nombre de la columna origen como parte de los parámetros del atributo.
Una cosa que debemos identificar para cada objeto es la llave primaria. En nuestro caso será la propiedad BookId . Aquí, combinamos el parámetro Name con un nuevo parámetro IsPrimaryKey para declarar el mapeado. LINQ para SQL requiere que al menos una propiedad de cada objeto sea especificada como llave primaria para administrar la identidad del objeto.
Usamos el mismo método para declarar los mapeados para cada una de las propiedades en nuestra clase. La declaración resultante se muestra a continuación.
A pesar que pueda parecer que se ha duplicado el número de líneas de código en la clase Book , el resultado total reducirá drásticamente el código, ya que no necesitaremos preocuparnos sobre la creación de métodos separados para los métodos Create, Read, Update y Delete (CRUD). Adicionalmente, no necesitaremos una implementación personalizada para operaciones de consultas especializadas. Declaramos el mapeado una vez y el marco de trabajo se ocupa del resto. A pesar que se ha especificado cómo acceder a las tablas y columnas, no podemos hacer nada a menos que identifiquemos la base de datos donde residen las tablas. Necesitamos configurar nuestra conexión a la base de datos. Hacemos esto usando el nuevo objeto DataContext ubicado en el espacio de nombres System.Data.Linq . Una vez hecho, pasamos justo a consultar los datos.
1.2. Configurando el DataContext.
El DataContext mostrado yace como el corazón de LINQ para SQL y maneja la mayoría del trabajo. Primero y más importante, maneja nuestra conexión a la base de datos. Le instruimos al DataContext sobre la cadena de conexión. El DataContext manejará la apertura y cierre de la conexión por nosotros. Como resultado, no necesitamos preocuparnos sobre el abuso de cada conexión hacia recursos externos.
Para empezar a trabajar con el DataContext , cree una instancia de un objeto DataContext enviándole la cadena de conexión para la base de datos.
El DataContext también maneja nuestros mapeados y proporciona un recurso vital - la habilidad para llenar una colección de objetos desde una base de datos. Llena el objeto dentro de un tipo de colección genérica especializada llamada Table<> . Para obtener una tabla de books desde el objeto DataContext , se llama a dataContext.GetTable() :
Sin LINQ para SQL, cuando se devuelve una lista de objetos, el tipo de retorno sería un List genérico. En este caso, estamos devolviendo un nuevo tipo - Table . Al hacer este cambio, no devolvemos los datos en bruto, sino los medios por los cuales podemos acceder y manipular dinámicamente los datos. Esto nos permitirá modificar la consulta antes de enviar la solicitud a la base de datos. Ahora que tenemos acceso a los datos, veamos lo que podemos hacer con LINQ para SQL más allá de esto.
2. Leyendo daos con LINQ para SQL.
Lo primero que necesitamos hacer es seleccionar los valores de la base de datos. Ya hemos visto una manera de acceder a los datos usando el método GetTable . La clase genérica Table implementa una nueva interface IQueryable , la cual se extiende a IEnumerable . Debido a que se extiende a IQueryable , tenemos libertad para usar los operadores de consulta estándar de LINQ para Objetos. Empecemos con una consulta básica que recupere todos los libros desde nuestro nuevo objeto refactorizado.
Con este ejemplo, hemos eliminado efectivamente cualquier código ADO .NET personalizado que de otra forma habríamos necesitado escribir. Sin embargo, recuperando todos los campos sin importar si los necesitamos usar. Como hemos aprendido las capacidades de LINQ para SQL, quisiéramos examinar nuestro código en la base de datos. En ocasiones, la consulta resultante puede ser sorprendente. Tenemos varias opciones para ver la consulta que es enviada hacia la base de datos. Usando la herramienta SQL Server Profiler que viene con SQL Server, podemos ver las sentencias mientras están siendo enviadas hacia la base de datos. Alternativamente, podemos anexar la propiedad Log del DataContext para un flujo de salida, como el que tiene Console:
Con esta función de registro habilitada, cualquier sentencia SQL enviada hacia la base dedatos será enviada hacia el flujo de salida. Si lo anexamos a la consola en una aplicación de consola, las sentencias aparecerán en la ventana de consola. En una aplicación de
formularios Windows, los resultados serán enviados a la ventana Output . Usaremos el registro frecuentemente, para ver qué está sucediendo detrás del escenario. Como otra alternativa, Microsoft tiene una herramienta QueryVisualizer que puede ser descargada separadamente de Visual Studio 2008. La herramienta, junto con el código fuente y las instrucciones de instalación, está disponible enhttp://weblogs.asp.net/scottgu/archive/2007/07/31/linq-to-sql-debug-visualizer.aspx. Una vez esta herramienta se encuentra instalada, se puede revisar el código y revisar los objetos de consulta instanciados para ver una nueva lupa como parte de la asistencia de depuración, como se muestra en la figura siguiente:
Haga clic sobre la lupa, y se abre la ventana mostrada a continuación, permitiendo el acceso a toda la sentencia SQL que serpa enviada. El visualizador también nos permite ver los resultados en una grilla de datos, y opcionalmente editar manualmente el código SQL generado.
También podemos acceder programáticamente la consulta usando el métodoGetCommand del DataContext , como se muestra:
Este comando no identificará cuándo la consulta es ejecutada, pero mostrará la sentencia que será enviada. Mientras se acostumbra a LINQ para SQL, pruebe cada una de estas técnicas para ver cuáles trabajan mejor para usted. Sin importar cuál seleccione, asegúrese de observar las sentencias que están siendo enviadas. Mientras aprende LINQ para SQL,
encontrará la necesidad de alternar consultas para evitar resultados inesperados que de otra forma no podría evitar. Devolvamos la atención a nuestra consulta. En los ejemplos previos, se mostró cómo podíamos usar los mapeados para obtener valores, pero en lugar de obtener sólo los campos que necesitamos, obtenemos la tabla book completa. Debido a que LINQ para SQL construye sobre expresiones de consulta, podemos proyectar las columnas que deseamos en el conjunto de resultados. Así, si sólo queremos obtener un listado de títulos de nuestra consulta, podríamos cambiar la cláusula select como se muestra a continuación:
Debido a que usamos dataContext.Log , podemos ver en la ventana Output la consulta resultante.
Al revisar esta sentencia SQL, vemos que ya no estamos devolviendo todas las propiedades de book desde nuestra base de datos. Casi hemos logrado la primera etapa: recuperar los títulos y precios de los libros. Para lograr esta meta, necesitamos cambiar la cláusula select para devolver un tipo anónimo con sólo los valores Title y Price.
Observe que el código SQL generado selecciona solamente los campos solicitados como parte de la extensión del método Select , en lugar de llenar todo el objeto book .
Intente de nuevo el ejemplo, pero esta vez recorra el código. Ponga atención a la ventana de consola. Observe que el código SQL no es insertado en la ventana cuando se llama al método dataContext.GetTable() , ni es mostrada cuando se declara al objeto query . De hecho, el SQL no es generado y enviado hacia la base de datos hasta que primero se cree el acceso a los datos. La variable query contiene la definición sobre cómo deseamos acceder los datos, no los mismos datos. La ejecución de la consulta es diferida hasta que sea usada por primera vez. Debido a que no se crea la consulta hasta que los resultados hayan sido solicitados por primera vez, podemos continuar construyendo la consulta ag regando más funcionalidades. Agregando funciones de paginado a la consulta después que es definida por primera vez usando los métodos de extensión Skip y Take. LINQ para SQL une entonces para crear una sola sentencia optimizada.
El código resultante es el siguiente:
LINQ para Objetos estándar habría emitido una sola sentencia SELECT que recuperara todos los registros en books. Debido a que LINQ para SQL fue lo suficientemente listo para detectar las operaciones adicionales, fue capaz de optimizar la consulta para ser específica a nuestra base de datos (SQL Server 2005). Si estuviéramos usando SQL Server 2000, una sintaxis diferente habría sido usada porque la opción ROW_NUMBER no está disponible antes de SQL Server 2005. Hemos visto un poco de la potencia que LINQ para SQL proporciona a la tabla. En lugar de una recuperación masiva de registros y apoyarse en LINQ para Objetos para realizar las pesada carga, LINQ para SQL tiene la potencia de evaluar nuestras solicitudes y devolver sólo los resultados solicitados. Si deseamos hacer operaciones d e selección adicionales incluyendo filtrado y ordenamiento, usamos la sintaxis de consulta SQL común. La expresión de consulta declarativa es convertida y ajustada según se requiera para satisfacer las necesidades del momento. Devolvamos nuestra atención sobre l a extensión de las consultas básicas de recuperación para agregar más funcionalidades.
3. Refinando las consultas.
Hasta ahora nos hemos enfocado en recuperar los resultados de una tabla. Se ha mostrado cómo LINQ es mejor que ADO .NET porque no necesita reescribir todo el código de plomería repetitivo. LINQ para SQL también es capaz de reducir el tráfico de red al devolver sólo los campos que necesitamos. Las bases de datos relacionales ofrecen capacidades especializadas para acceder y manipular conjuntos de datos asociados. Al balancear el indexado y la ejecución de planes, la base de datos proporciona un acceso de datos más rápido de lo que podríamos tener sin los índices. Adicionalmente, al procesar la consulta en el servidor, a menudo podemos limitar la cantidad de información que debe ser transmitida sobre la red. El reducir la demanda de red es importante porque el canal de red es típicamente uno de los mayores cuellos de botella de aplicaciones centradas en datos. Continuemos nuestra revisión de LINQ para ver cómo
podemos refinar nuestras consultas usando algunos procesos del lado del servidor adicionales.
3.1. Filtrado.
LINQ para SQL soporta un amplio rango de funcionalidades de filtrado. Un filtrado puede ser tan simple como encontrar un registro con un valor específico. En nuestro ejemplo, queremos ver los libros que cuestan menos de 30 dólares. Podemos lograr esto con el código siguiente:
Si vemos el código SQL generado, el resultado es justo el esperado:
Una consulta basada en objeto recuperará todos los registros de la base de datos. Cuando se usa LINQ para SQL, podemos traducir la cláusula de filtrado en una consulta parametrizada que es ejecutada en el servidor, limitando los resultados de los registros que cumplen el criterio. Adicionalmente, al usar consultas parametrizadas, solucionamos un par de asuntos comunes. Primero, una de las más grandes vulnerabilidades de seguridad es la habilidad de inyectar funcionalidad dentro de una consulta (como borrar una tabla). Una de las maneras más sencillas para impedir este tipo de vulnerabilidad, llamada ataque de SQL inyectado , es usar consultas parametrizadas o procedimientos almacenados. Otra ventaja de usar consultas parametrizadas es el hecho que podemos aprovechar el alojamiento en memoria del plan de consulta de SQL Server. Al reutilizar consultas donde el único cambio son los parámetros de entrada, SQL Server puede determinar un plan de ejecución adecuado y alojarlo en memoria para su uso posterior. En solicitudes siguientes, el servidor usará el plan de ejecución en memoria, en lugar de reevaluar la expresión. Si se concatena la solicitud SQL, el servidor necesitaría convertir la expresión cada vez para determinar el plan de ejecución más eficiente basado en los índices disponibles.
Algunas opciones SQL de filtrado no tienen una traducción directa a palabras reservadas en el .NET Framework. En muchos casos, hay una alternativa que reali za lo mismo o una función similar. Cuando puede, LINQ traducirá la llamada de función a su equivalente SQL. Consideremos la cláusula SQL LIKE . LIKE encuentra registros basados en un esquem de coincidencia de patrones. En su lugar, el tipo String tiene tres métodos que realizan la misma función - StartsWith, EndsWith y Contains . LINQ para SQL ha sido diseñado para mapear esas funciones a la expresión LIKE usando el método sqlMethods.Like e inserta el comodín que coincide con el patrón, según sea necesario. Así, para encontrar todos los libros conteniendo la cadena "on", usamos la expresión LINQ mostrada a continuación:
La consulta usando el método Contains traduce hacia la expresión SQL siguiente:
Observe que el método Contains ha sido traducido a LIKE y el valor del parámetro ahora incluye el comodín %, el cual es específico de SQL Server. No todas las funciones CLR pueden ser traducidas a un equivalente de bases de datos. Considere la consulta siguiente:
En este ejemplo, el proveedor de traducciones es capaz de convertir el método DateTime.Parse e inserta una representación específica de base de datos para la fecha. No es capaz de manejar el método ToString para dar formato a los datos en la cláusulaselect . El identificar todas las expresiones soportadas y no soportadas que son
traducidas es imposible. Adicionalmente, el soporte de traducción depende del proveedor. Cuando no esté seguro si un método es soportado, inténtelo y vea si funciona. En muchos casos, el filtrado trabaja como se espera. En otros casos, la experimentación es necesaria para encontrar los métodos apropiados. No se puede cubrir todo el mapeado aquí, pero tenemos suficiente para iniciar. Al permitir que el filtro sea aplicado en el servidor en lugar de en el cliente, podemos reducir enormemente la cantidad de ancho de banda de red y aprovechar los índices de la base de datos. Hasta ahora, hemos sido capaces de reescribir la consulta original y los objetos para devolver sólo los campos deseados y los registros de la base de datos mientras eliminamos el código ADO .NET. Continuemos refinando la consulta agregando ordenamiento.
3.2. Ordenamiento y agrupamiento.
Si necesitamos efectuar algunas funciones de ordenamien to manualmente, necesitaríamos escribir mucho código personalizado. LINQ para Objetos nos permite simplificar la consulta, pero para utilizar verdaderamente la potencia de la base de datos, necesitamos usar los índices que la base de datos ha definido. La expresión de consulta orderby y orderby...descending están diseñadas para traducir nuestra expresión de ordenamiento hacia la base de datos. Considere el cambio que hicimos a nuestra consulta agregando la función de ordenamiento, tal como se muestra:
Como se indicó antes, esta consulta es un verdadero ejemplo de WYWIWYM (Lo que se mira es lo que se desea expresar - What You See Is What You Mean). Como se ha visto en la cadena de consulta resultante, ahora hemos logrado otra parte de nuestra meta - balancear los índices de la base de datos para manejar ordenamiento en lugar de ordenar del lado del cliente.
Si quisiéramos ordenar los resultados en orden descendente, usaríamos la expresión de consulta descending como parte de la cláusula. Además, si se desea ordenar sobre varias columnas, incluiríamos la lista de campos separados por comas tal como lo haríamos con una expresión SQL estándar. A menudo, en lugar de sólo ordenar los resultados, necesitamos agrupar los resultados. En el código siguiente, agrupamos la lista de libros por tema. Proyectamos los resultados de la operación de agrupamiento en un resultado temporal que luego podemos reutilizar.
El objeto resultante es una colección ordenada de colecciones. Para ver los resultados, necesitamos iterar sobre ambos resultados: los agrupamientos y la colección de libros contenida para cada grupo de temas. Esto producirá la salida SQL siguiente:
Observe que esta consulta sólo selecciona los valores llave. Como iteramos sobre resultados, consultas separadas son emitidas para cada grupo. La colección resultante es contenida en el
objeto groupedBooks . Mientras tenemos nuestros resultados agrupados, sería bueno si pudiéramos desarrollar alguna agregación sobre los valores para que podamos ver conteos, promedios y totales por grupo.
3.3. Agregación.
LINQ para SQL soporta totalmente todos los métodos e stándar de agregación que extiende IEnumerable . Así, podemos crear una consulta para mostrar el número de libros que pertenecen a cada categoría. El código siguiente usa un tipo anónimo de nuestra cláusula select para tomar nuestra colección de libros y devolver la cuenta de los libros por tema.
Observe que en este ejemplo, podríamos devolver todos los libros mientras iteramos sobre el conjunto de resultados y luego contarlos en el cliente. LINQ para SQL ofrece el beneficio adicional de desarrollar la cuenta en el servidor y devolver sólo el valor en lugar de sobrecargar la red con datos innecesarios. Aquí está la sentencia SQL correspondiente para esta consulta:
Continuamos con la tradición de devolver sólo los resultados que deseamos y no sobrecargar nuestra base de datos o la red con datos innecesarios. El usar los otros métodos agregados también es fácil. El código siguiente agrega el precio total, el precio más bajo, el precio más alto, y el precio promedio de los libros agrupados por tema.
Una vez más, los métodos de agregación son traducidos hacia el SQL apropiado y la agregación es desarrollada en la misma base de datos. La base de datos devuelve sólo los resultados que le solicitamos, limitando la cantidad de datos que necesitamos devolver. Hasta ahora se ha trabajado sólo con valores de una sola tabla. Sería bueno si se pudiera unir la tabla Book con la tabla Subject correspondiente, de forma que pudieramos incluir el nombre descriptivo del tema en lugar de un identificador críptico único contenido en la tabla Book . Naturalmente, LINQ para SQL ofrece varias opciones para unir los resultados.
3.4. Unión.
El combinar datos de múltiples tablas es el corazón y alma de las bases de datos relacionales. Si no necesitáramos combinar diferentes partes de da tos, seríamos felices escribiendo aplicaciones empresariales en Excel o en archivos de texto planos. Al ser capaces de relacionar datos, somos capaces de profundizar en información que de otra forma estaría oculta en regiones individuales. En nuestro caso, vamos a unir la tabla Books con la tabla Subjects. De esta manera, podemos mostrar el nombre del tema en lugar de sólo la llave foránea. LINQ para SQL soporta dos sintaxis para unir. La primera usa una comparación en la cláusula Where, la cual es similar a la sintaxis SQL ANSI-89. Para usar esta sintaxis, podemos obtener una referencia a los objetos de tabla Book y Subject . Observe que no se dijo que íbamos a recuperar las tablas. Con las referencias a los objetos tabla, el código siguiente muestra cómo podemos componer nuestra expresión de consulta seleccionando de ambas tablas, donde SibjectId del objeto Subject es lo mismo queSubjectID del correspondiente objeto Book .
Hace más de 15 años, el e l estándar ANSI-92 reemplazo al estándar ANSI-89. Volver haci a la antigua sintaxis ANSI-89 puede parecer inusual. Afortunadamente, LINQ también soporta la sintaxis join del estándar SQL ANSI-92. La expresión de consulta anterior puede ser reescrita de la manera siguiente:
Tenga en cuenta que el orden de los objetos origen y destino es importante en las cláusulas de unión LINQ. A diferencia de la naturaleza permisiva de SQL interpretado por la base de datos, LINQ es menos permisivo. Debido a que las expresiones de consulta son traducidas a métodos, cambiar el orden de las tablas pero no los campos resultará en un error en tiempo de compilación. Aquí está la definición para la extensión del método System.Enumerable.Linq.Join .
Observe cómo el primer y tercer parámetro coinciden, así co mo el segundo y el cuarto. El bloque de código siguiente muestra cómo la cláusula Join en nuestra consulta mapea hacia los parámetros del método de extensión. Podemos ver cómo los parámetros outer y y outerKeySelector coinciden. Si fuéramos a transponer los parámetros outer e e inner o o los correspondientes innerKeySelector y outerKeySelector , terminaríamos con un desajuste en nuestros parámetros cuando los traducimos hacia el método de extensión subyacente.
Hasta ahora, cada una de estas uniones han sido una unión cruzada (cross join) o una unión interna (inner join), donde solamente sol amente devolvemos los valores que tienen resultados similares en ambas tablas. Pero, a menudo deseamos devolver resultados de una tabla sin importar si hay resultados coincidentes en la otra tabla. En términos de SQL estándar, esto es referido típicamente como una unión externa (outer join). En el ejemplo anterior, se desea obtener un listado de todos los temas sin importar si existe algún libro para p ara dicho tema. Esto típicamente sería expresado con la siguiente expresión exp resión SQL ANSI-92:
Para lograr lo mismo con LINQ, L INQ, necesitamos observar que estamos buscando libros donde existe el tema o es nulo. El método de extensión DefaultIfEmpty() viene a nuestro rescate, como se muestra a continuación:
En este caso, le decimos a LINQ que queremos unir los libros con los temas y colocar los resultados en un nuevo objeto temporal llamado joinedBooks joinedBooks. Luego queremos ver los resultados de la unión de Subjects y Books, usando el método de extensiónDefaultIfEmpty para devolver un valor por defecto si el tema no contiene un libro. Ahora que podemos combinar los libros con los temas, volvamos a la consulta original que iniciamos al principio de este material para ver cuánto hemos avanzado. A continuación se muestra el resultado final.
Comparando esta consulta, podemos ver que el único cambio real es el origen de datos. La consulta LINQ es idéntica. Pero una mirada rápida en la sentencia SQL generada muestra que ahora se han recuperado sólo sól o las filas y columnas que deseamos. Adicionalmente, estamos ejecutando la unión, filtrado y ordenamiento en el servidor. Aquí está el SQL que es generado desde nuestra consulta LINQ para SQL:
La expresión LINQ es más explícita que la sentencia SQL correspondiente debido al hecho que LINQ está diseñado para trabajar no sólo con datos relacionales, sino además con otros orígenes de datos también, incluyendo orígenes de objetos y jerárquicos. Ya que hemos logrado nuestra meta, podríamos fácilmente detenernos aquí, pero LINQ para SQL ofrece más funcionalidades de lo que hemos visto. A pesar que hay ocasiones donde es necesario forzar una construcción relacional dentro de un modelo orientado a objetos, el trabajar directamente con jerarquía de objetos puede a menudo ser más apropiado para el desarrollo de aplicaciones.
4. Trabajando con árboles de objetos.
En el corazón del desajuste de impedancia objeto-relacional está el choque entre filas de datos unidos por columnas de identificación (relacional) y construcciones de memoria conteniendo colecciones de objetos (orientadas a objetos). Esos objetos pueden contener colecciones adicionales de objetos. Así, donde nuestra base de datos contiene filas de libros y temas que podamos unir, no tiene una forma fácil para leer un tema y luego mostrar automáticamente los libros asociados con ese tema. Necesitamos explícitamente decirle a la base de datos que una las dos tablas para devolver los resultados. En un mundo típicamente orientado a objetos, podemos profundizar para identificar los libros que pertenecen a un tema. LINQ para SQL ofrece una forma fácil de navegar esas jerarquías de objetos. Si regresamos a la definición de la clase Subject , tal vez determinemos que nos gustaría un método que nos permita profundizar dentro de los libros que pertenecen a ese tema. Típicamente, haríamos eso cargando perezosamente los libros relacionados a cada tema mientras los llamamos. Books entonces sería una propiedad del objeto Subject donde podríamos pofundizar y trabajar como quisiéramos. La funcionalidad de mapeo en LINQ para SQL siguiente muestra cómo expone nuestra colección de objetos Book como un objeto genérico System.Data.Linq.EntitySet y llamarla . De nuevo, usaremos la sintaxis de propiedad autoimplementada por brevedad.
Tal como las tablas y columnas, necesitamos decirle al framework cómo los objetos son relacionados. Haremos eso usando el atributo Association. Para que trabaje la asociación, necesitamos identificar cómo nuestro tipo Book está relacionado con Subject . Asociamos los dos objetos especificando la propiedad del objeto relacionado que uniremos con nuestro objeto actual. Nuestro objeto Book contiene una propiedad SubjectId que ya esté mapeada al campo Subject en la tabla Book en la base de datos. Así que, para la propiedad contenida en Books de la clase Subject , especificamos que la propiedad de la clase Books que representa la llave para nuestro registro es llamada SubjectId . Esta llave es OtherKey , o la propiedad llave en el objeto relacionado. Ahora que hemos especificado la relación entre los dos objetos, podemos alimentar a ISubjects usando una expresión estándar LINQ para SQL, como se muestra en el código siguiente. En lugar de unir tablas específicamente, podemos profundizar directamente dentro de la colección books de cada objeto. Para mostrar los resultados, iteraremos en Subjects . Mientras recorremos dentro de cada tema, vamos a anidar un lazo que muestre los nombres de libros que están en ese tema.
Cuando ejecutamos la consulta, podemos observar que por defecto, logramos los mismos resultados que una unión externa (outer join). Desde la perspectiva de un objeto, cuando alimentamos la lista de temas, no sabemos si tienen algunos libros asociados. Es sólo cuando iteramos sobre los libros de cada tema que encontramos si hay temas sin libros asociados. Por lo que debe haber casos donde mostramos un tema que no contiene ningún libro. De igual forma, si tenemos un libro que no tiene un tema, no aparecerá en el listado resultante. Para filtrar nuestros resultados un poco, tenemos a nuestra disposición una pareja de métodos de extensión adicionales: Any y All . El método Any sólo devuelve resultados donde los registros relacionados existen en ambos conjuntos de resultados. Por lo que, si se desea refinar la consulta para que devuelva sólo los temas que también tienen libros relacionados (similar a una unión interna - inner join), el código siguiente usa el método de extensión Any .
Si deseamos, podemos simplemente negar la cláusula where de la consulta para devolver cualquier tema donde no haya ningún libro relacionado, como se muestra:
Si deseamos filtrar los resultados y ver sólo los temas donde el precio del libro es menor a 30 dólares, podemos usar el método de extensión All como se muestra:
La habilidad para representar nuestros datos por medio de una jerarquía de objetos más natural nos permite trabajar con ellos en una forma de programación más familiar. Establecemos las dependencias de objetos basados en las necesidades específicas del negocio y podemos trabajar con ellos como lo haríamos con cualquier conjunto de objetos. Esto nos permite mantener nuestras reglas de negocios e integridad sin tener que enfocarnos en la naturaleza relacional del almacén subyacente. Si deseamos, podemos reescribir nuestro ejemplo usando una sintaxis jerárquica como se muestra a continuación:
En esta versión, no sólo implementamos las jerarquías de objeto de forma más natural (1), sino que además anida los resultados como un conjunto similar de estructuras de objeto jerárquicas (2). De nuevo, dejamos que la base de datos haga lo que mejor sabe hacer y sólo devuelve las porciones necesarias de los objetos subyacentes. Hay ocasiones donde deseamos consultar elementos que no están relacionados. En esos casos, la unión de datos todavía es requerida. Ambas opciones están disponibles según demanda las necesidades del negocio. Sin importar cuál método trabaja mejor para cada situación, LINQ para SQL intenta devolver sólo los valores solicitados y sólo los devuelve cuando es necesario. Usualmente, esto es una ventaja. Ocasionalmente el comportamiento de la carga perezosa resulta en una interacción más frecuente con la base de datos de lo que fue la intención originalmente. Continuemos nuestra explicación de LINQ para SQL mirando en ocasiones donde el comportamiento por defecto puede resultar en solicitudes más frecuentes a bases de datos.
5. ¿Cuándo son cargados los datos y por qué interesa?
Cuando se recuperan datos de la base de datos, LINQ para SQL utiliza una técnica llamada carga perezosa. Con la carga perezosa, los resultados sólo son cargados a la memoria cuando son solicitados. Revisando los ejemplos mostrados en este material y poniendo atención a cuándo las sentencias SQL son generadas, podemos ver que no son generadas cuando se define la consulta. En su lugar, la base de datos no es accedida hasta que realmente solicitemos cada registro. Esperar a acceder a los valores hasta que sean necesarios es llamado carga perezosa.
5.1. Carga perezosa.
Cuando se muestran los resultados, la carga perezosa ofrece los beneficios de sólo recuperar los datos cuando se soliciten, y sólo devolver los datos solicitados. En muchos casos, esto proporciona beneficios de rendimiento, pero en otros casos puede conducir a algunos resultados inesperados. Considere el código siguiente, el cual muestra la lista de temas que tienen o no tienen libros asociados. En este caso, enviaremos los comandos SQL generados a la ventana de consola para ser mostrados cuando la solicitud es enviada a la base de datos. Cuando ejecutamos este ejemplo, revise el código y observe cuidadosamente la ventana de consola para ver exactamente cuándo es emitido cada comando.
Debido a que sólo queremos listar los temas, no estamos recuperando los libros. Al recuperar sólo los libros si y cuando realmente los necesitamos, podemos optimizar el ancho de banda de red, minimizar el consumo de memoria, y limitar la cantidad de trabajo que la base de datos necesita hacer. Podemos extender este código instruyéndole a ObjectDumper que no sólo muestre Subjects , sino que también muestre sus hijos diciéndole que deseamos ver cada nivel hijo además del nivel base. El método ObjectDumper.Write acepta una sobrecarga para indicar el nivel que deseamos ver. Cambie la última línea para solicitar los hijos del primer nivel, como se muestra:
Observe que el registro hijo para cada tema es recuperado sólo cuando se desea. Este comportamiento de carga perezosa es benéfico cuando no necesitamos mostrar todos los
hijos de todos los objetos padre, sino que sólo deseamos recuperarlos a medida que el usuario solicita los detalles. Como solicitamos los libros de cada tema, enviaremos una consulta separada a la base de datos para cada registro de la tabla Subject . A continuación se muestra la salida de la versión cambiada del código.
En el código generado, insertamos la lista de temas, luego mientras iteramos en cada elemento, emitimos una consulta separada para cada libro, pasándole la columna id del tema para cada registro. Esto significa que en lugar de emitir una sentencia, enviamos sentencias separadas para cada registro hijo. Antes de mejorar esta situación, empeorémosla. En este caso, copie la última línea y péguela dos veces para que llamemos dos veces a ObjectDumper.Write . Revisemos el código y pongamos atención al SQL que es generado. Se evitará tener que leer nuevamente los resultados. En este caso, toda la consulta fue enviada dos veces a la base de datos, una vez por cada método Write. Ahora hemos transformado una cosa muy buena (carga perezosa) en una cosa potencialmente mala (demasiado tráfico de red a la base de datos). ¿Qué podemos hacer para mejorar la situación?
5.2. Cargando detalles inmediatamente.
Si todo lo que deseamos es cargar los resultados más de una vez, podemos precargarlos y almacenarlos en un arreglo o lista usando los métodos de extensión ToList , ToDictionary , ToLookUp o ToArray . Así, podríamos cambiar nuestra primera implementación para indicar que deseamos cargar todos los clientes una sola vez y luego continuar con los resultados mientras la variable subjects esté al alcance.
Al establecer explícitamente que deseamos recuperar los resultados, obli gamos a LINQ para SQL que cargue los datos inmediatamente y que llene una nueva lista genérica ( List ) con los objetos Subject resultantes. Esto tiene la ventaja de no requerir viajes redondos hacia la base de datos cada vez que se desea recuperar nuestra lista. Adicionalmente, podemos usar mecanismos de consulta LINQ para Objetos, para continuar manipulando l os resultados y desarrollar agrupamientos, unión, agregación y ordenamiento según sea necesario. Desafortunadamente, al convertir nuestros resultados en List o Array , perdemos algo de los beneficios de LINQ para SQL, específicamente la habilid ad de optimizar los datos que recuperamos de la base de datos aprovechando la funcionalidad del lado del servidor y limitando la cantidad de datos que tenemos que colocar en memoria. Cualquier expresión de consulta aplicada a la lista de temas resultante será procesada por LINQ para Objetos en lugar de LINQ para SQL. El sólo convertir los resultados con ToList no ayuda a eliminar las consultas separadas para recuperar cada colección hija de la carga perezosa. Afortunadamente, LINQ para SQL soporta un mecanismo que le instruye al DataContext cuál optimización debe hacer. Al usar el tipo DataLoadOptions , se puede dar forma (pero no recuperar) los conjuntos de resultados por adelantado. Tan pronto como un registro del tipo de objeto declarado es recuperado, también recuperará los registros hijos asociados.
En este ejemplo, creamos un nuevo objeto llamado DataLoadOptions (1). La función principal de DataLoadOptions es indicar cuál objeto hijo carga con un tipo de objetos determinado. Ya que deseamos asegurarnos que cargamos nuestros libros cuando se quiera cargar los temas, le indicamos a las opciones que carguen LoadWith y se le pasa una función en forma de una expresión (2). Podríamos pasar un delegado real, pero debido a que tenemos expresiones lambda a nuestra disposición, le podemos decir, "dado un tema, cargue el conjunto de entidades Books ". Todo lo que queda es anexar nuestras opciones a nuestro contexto de datos (3). Con las nuevas opciones en su lugar, se ejecuta el ejemplo nuevamente. Aquí está el SQL que genera LINQ:
Al especificar el DataLoadOptions de los datos que deseamos acceder, eliminamos las subconsultas múltiples que eran necesarias en la implementación de código perezoso anterior. Esto proveería una interacción mucho más mejorada entre nuestra aplicación cliente y la base de datos. Por supuesto, si sabemos que no necesitaremos los libros, no los deberíamos cargar, pero si sabemos que necesitaremos trabajar con los libros para cada categoría, podemos cargarlos y proporcionar una aplicación que responda más. Tome en cuenta que las opciones de carga pueden ser definidas sólo una vez en una instancia de un contexto. Una vez definida, no puede ser cambiada para esa instancia. El usar DataLoadOptions ofrece un poderoso control sobre los resultados deseados, pero requiere más cuidado cuando se usa. Sólo con especificar DataLoadOptions no eliminamos las múltiples cargas si tratamos de iterar dos veces sobre los resultados. Para finalizar nuestra optimización, podemos combinar DataLoadOptions con el método ToList . El usar ambos juntos (DataLoadOptions y ToList ) nos asegura el acceso a la base de datos una vez y también asegura que los temas y los libros se unen de forma apropiada. El usar uniones le da a LINQ para SQL una poderosa habilidad para profundizar en los datos. Ya sea que seleccionemos trabajar de una manera orientada a objetos o emulando una interacción relacional, podemos especificar el mapeado una vez y luego enfocarnos en las necesidades del negocio. Debemos ser cuidadosos y revisar la interacción de base de datos subyacente para asegurarnos que la estamos optimizando como debemos. En muchas operaciones sencillas, el comportamiento por defecto está bien. Sin embargo, hay ocasiones en las cuales refactorizar nuestro código puede producir mejoras dramáticas en la implementación resultante. Si realmente deseamos control sobre los da tos que recibimos, tenemos opciones adicionales como procedimientos almacenados y funciones definidas por el usuarios.
6. Actualizando datos.
Si estuviéramos limitados para recuperar datos, la funcionalidad no sería más que una herramienta de reportes. Afortunadamente, actualizar datos es tan simple como recuperarlos. Mientras tengamos un objeto DataContext persistente, podemos realizar adiciones, modificaciones y eliminaciones usando métodos estándar en los objetos tabla. El DataContext guarda registros de los cambios y maneja la actualización de la base de datos con un solo método de llamada. Para empezar, veamos un ejemplo que actualiza el precio de nuestros libros más caros para que podamos ofrecer un descuento sobre ellos. En este caso, recuperamos sólo los libros que son "caros" (donde el precio es mayor a $30) y luego iteramos sobre ellos, reduciendo el precio de cada uno en $5. Finalmente, persistimos los cambios de nuestra base de datos llamando el método SubmitChanges en nuestro objeto DataContext .
En el código anterior, se inicia recuperando los registros que se desea modificar (1). Luego realizamos los cambios necesarios trabajando en el objeto book como lo haríamos con cualquier otra colección (2). Una vez finalizado, llamamos a SubmitChanges para aplicar los cambios (3). No necesitamos preocuparnos sobre crear un mapeado separado para emitir y actualizar una instrucción. El contenido toma el mismo metadato que creamos para consultar y usarlo para generar la sentencia update necesaria. Aquí está el SQL que fue generada por nuestro ejemplo:
A pesar que este código puede parecer ser excesivo, las primeras dos líneas efectúan la actualización. El resto está ahí para revisar violaciones de concurrencia. Lo que es importante en este punto es observar que el cambio de administrador del objeto DataContext reconoció que la única columna que necesita ser cambiada en nuestra base de datos es el campo Price. No trata de actualizar ninguna otra columna o registro. Esto reduce la cantidad de datos que necesitamos transmitir en la red. También somos capaces de enviar múltiples actualizaciones en una sola unidad de trabajo y aplicarlos con una sola llamada a SubmitChanges . Hemos visto cómo leer y actualizar. Ahora, tomemos un vistazo a las otras dos partes de la operación CRUD: create ydelete . Típicamente cuando se trabaja con colecciones, agregaríamos y borraríamos obj etos usando los métodos Add y Remove de IList . La semántica tradicional de Add y Remove implica que las colecciones reflejan inmediatamente los nuevos valores. Libe raciones previas de LINQ continuaban la tradición de usar Add y Remove para estas funciones. Sin embargo, los usuarios se confundían cuando sus valores no eran devueltos como parte de consultas subsecuentes hasta que eran aplicadas a la base de datos. Como resultado, los nombres para estos métodos fueron cambiados a InsertOnSubmit y DeleteOnSubmit para reflejar la naturaleza de la implementación más exactamente. El crear nuevos elementos con LINQ para SQL es tan simple como llamar al método InsertOnSubmit en el objeto table. Para eliminar, similarmente llamamos al método DeleteOnSubmit . El código siguiente demuestra el agregar un libro a la colección de libros y posteriormente borrarlo.