El Marco de Entidades de ADO.NET
Junio de 2006
Traducido por César de la Torre
Este artículo no ha sido traducido por Microsoft
Este artículo y la veracidad de su traducción no ha sido revisado o verificado por Microsoft.
Microsoft no acepta responsabilidades sobre la veracidad o la información de este artículo que se proporciona tal cual por la comunidad.
En esta página
Introducción
Marco de Entidades de ADO.NET: Modelando al nivel de abstracción adecuado
Marco de Entidades de ADO.NET: Servicios de Objetos
LINQ to Entities: Consultas integradas en los lenguajes
Referencias
Apéndice
Introducción
Cuando se analiza la cantidad de código que el desarrollador de aplicaciones medio debe escribir para atacar el desajuste de impedancias entre las diferentes representaciones de datos (por ejemplo, objetos y almacenes relacionales), queda claro que hay posibilidades de mejorar. De hecho, hay muchos escenarios donde un marco de trabajo adecuado puede potenciar a un desarrollador de aplicaciones para que se centre en las necesidades de de la aplicación y no en las complejidades relacionadas con combinar diferentes representaciones de datos.
Un objetivo central de la próxima versión de ADO.NET es elevar el nivel de abstracción de la programación de datos, ayudando así a eliminar el desajuste de impedancias entre los modelos de datos y los lenguajes con los que los desarrolladores de aplicaciones tendrían de otra forma que manejar. Dos innovaciones que hacen posible este avance son las Consultas Integradas en los Lenguajes (Language Integrated Query – LINQ) y el Marco de Entidades de ADO.NET (ADO.NET Entity Framework). El Marco de Entidades existe como una nueva parte de la familia de tecnologías ADO.NET. ADO.NET permitirá utilizar LINQ sobre muchos componentes de acceso a datos: LINQ to SQL, LINQ to DataSet y LINQ to Entities
Este documento describe el Marco de Entidades de ADO.NET, a qué espacio de problemas está dirigido y cómo sus diferentes componentes resuelven esos problemas.
1.1 A dónde queremos llegar
Un entorno ideal para la creación de aplicaciones de negocio consistiría en permitir a los desarrolladores describir la lógica de negocio y el estado del dominio del problema que están modelando con un mínimo o ningún “ruido” proveniente de la representación subyacente y de la infraestructura que la soporta. Las aplicaciones deben ser capaces de interactuar con los almacenes que mantienen el estado persistente del sistema en los términos del problema; específicamente, en los términos de un modelo conceptual del dominio, completamente separado del esquema lógico del almacén subyacente.
Podríamos esperar que los desarrolladores fueran capaces de escribir código como el siguiente fragmento:
// accederemos al almacén de seguimiento de pedidos
using(OrderTracking orderTracking = new OrderTracking()) {
// buscar todos los pedidos pendientes
// de los vendedores de Washington
var orders = from order in orderTracking.SalesOrders
where order.Status == "Pending Stock Verification" &
order.SalesPerson.Estado == "WA"
select order;
foreach(SalesOrder order in orders) {
// obtener una lista de los objetos StockAppProduct
// a utilizar para la validación
List<StockAppProduct> products = new List<StockAppProduct>(
from orderLine in order.Lines
select new StockAppProduct {
ProductID = orderLine.Product.ID,
LocatorCode = ComputeLocatorCode(orderLine.Product)
}
);
// asegurarse de que hay existencias de todos los productos
// de este pedido a través del sistema de gestión de almacén
if(StockApp.CheckAvailability(products)) {
// marcar el pedido como "enviable"
order.Status = "Shippable";
}
}
// si hemos marcado uno o más pedidos como enviables,
// persistir los cambios en el almacén
orderTracking.SaveChanges();
}
Hay dos elementos a destacar en el código anterior:
-
No hay ninguna construcción artificial. Es común ver aplicaciones que necesitan adaptarse a las peculiaridades del esquema del almacén subyacente. Por ejemplo, las aplicaciones creadas sobre la base de bases de datos relacionales deben hacer un uso extensivo de encuentros para poder navegar a través de relaciones. En el código anterior, por el contrario, la “forma” de los datos sigue las abstracciones del problema que se modela; hay “pedidos”, que tienen “líneas de pedido” y que están asociadas a “vendedores”.
-
No hay fontanería. El código es muy intensivo en el trabajo con la base de datos, pero no hay objetos de conexión a la base de datos, no hay un lenguaje externo como SQL para formular las consultas, no hay asociación de parámetros ni configuración alguna embebida en el código. En este sentido, podríamos decir que este código es “pura lógica de negocio”.
Esta es la clase de expresividad y nivel de abstracción que ADO.NET, y en particular LINQ y el Marco de Entidades trabajando juntos, llevan al desarrollo de aplicaciones.
El resto de este documento describe en detalle los diferentes elementos que trabajan conjuntamente para que el ejemplo anterior funcione.
Marco de Entidades de ADO.NET: Modelando al nivel de abstracción adecuado
Toda aplicación de negocio tiene, explícita o implícitamente, un modelo conceptual de datos que describe los diferentes elementos del dominio del problema, así como la estructura de cada elemento, las relaciones entre cada elemento, sus restricciones, etc.
Debido a que actualmente la mayoría de las aplicaciones se apoyan en bases de datos relacionales, más tarde o más temprano tienen que tratar con datos representados de una manera relacional. Incluso si se hubiera utilizado un modelo conceptual de más alto nivel durante el diseño, ese modelo tradicionalmente no es directamente “ejecutable”, por lo que debe ser traducido a una forma relacional y aplicada a un esquema lógico de base de datos y al código de la aplicación.
Aún cuando el modelo relacional ha sido extremadamente efectivo en las últimas décadas, es un modelo orientado a un nivel de abstracción que frecuentemente no es apropiado para modelar la mayor parte de las aplicaciones de negocio creadas con entornos de desarrollo modernos.
Utilicemos un ejemplo para ilustrar este aspecto. El siguiente es un fragmento de una variación de la base de datos de ejemplo AdventureWorks incluida en Microsoft SQL Server 2005:
Figura 1
Si estuviéramos desarrollando una aplicación de recursos humanos sobre esta base de datos y en algún momento quisiéramos encontrar todos los empleados a tiempo completo contratados durante 2006 y listar sus nombres y títulos, tendríamos que escribir la siguiente consulta SQL:
SELECT c.FirstName, e.Title FROM Employee e INNER JOIN Contact c ON e.EmployeeID = c.ContactID WHERE e.SalariedFlag = 1 AND e.HireDate >= '2006-01-01'
Esta consulta es más compleja de lo que debería ser por varias razones:
-
Aunque esta aplicación específica solo trata con “empleados”, aún tiene que tratar con el hecho de que el esquema lógico de la base de datos está normalizado, por lo que la información de contacto de los empleados –por ejemplo sus nombres- está en una tabla independiente. Aunque esto no concierne a la aplicación, los desarrolladores deberán incluir este conocimiento en todas las consultas de la aplicación que traten con empleados. En general, las aplicaciones no pueden elegir el esquema lógico de la base de datos (por ejemplo, las aplicaciones departamentales que exponen datos del núcleo del sistema de base de datos de la empresa), y el conocimiento de cómo mapear el esquema lógico a la vista “apropiada” de los datos que la aplicación requiere está expresado implícitamente a través de consultas a lo largo de todo el código
-
Esta aplicación de ejemplo solo trata con los empleados a tiempo completo, por lo que idealmente no tendría que ver empleados de ninguna otra clase. Sin embargo, como la base de datos es compartida, todos los empleados están en la tabla Employee, y se clasifican mediante una columna “SalariedFlag”; esto nuevamente implica que cada consulta emitida por esta aplicación tendrá embebido el conocimiento de cómo distinguir un tipo de empleado de otro. Idealmente, si la aplicación trata con un subconjunto de los datos, el sistema solo debería presentar ese subconjunto de los datos, y los desarrolladores deberían poder indicar de manera declarativa cuál es el subconjunto apropiado.
Los problemas anteriormente señalados tienen relación con el hecho de que el esquema lógico de la base de datos no siempre es la vista correcta de los datos para una aplicación dada. Observe que en este caso particular, una vista más apropiada podría ser creada utilizando los mismos conceptos utilizados por el esquema existente (esto es, tablas y columnas tal y como existen en el modelo relacional). Hay otros aspectos que se revelan al crear aplicaciones centradas en datos que no se modelan fácilmente usando únicamente las construcciones suministradas por el modelo relacional.
Piense ahora en otra aplicación, esta vez el sistema de ventas, también creada sobre la misma base de datos. Utilizando el mismo esquema lógico del ejemplo anterior, tendríamos que utilizar la siguiente consulta para obtener la lista de todos los vendedores con pedidos por encima de los $200.000:
SELECT SalesPersonID, FirstName, LastName, HireDate FROM SalesPerson sp INNER JOIN Employee e ON sp.SalesPersonID = e.EmployeeID INNER JOIN Contact c ON e.EmployeeID = c.ContactID INNER JOIN SalesOrder o ON sp.SalesPersonID = o.SalesPersonID WHERE e.SalariedFlag = 1 AND o.TotalDue > 200000
Nuevamente, la consulta es demasiado complicada en comparación con la pregunta relativamente simple que estamos haciendo al nivel conceptual. Entre las causas de esta complejidad están:
-
Nuevamente, el esquema lógico de la base de datos es demasiado fragmentado, e introduce una complejidad que la aplicación no necesita. En este ejemplo, la aplicación probablemente solo está interesada en “vendedores” y “pedidos”; el hecho de que la información sobre los vendedores está diseminada a lo largo de tres tablas no es de interés, pero aún así es conocimiento que el código de la aplicación debe tener.
-
Conceptualmente, sabemos que un vendedor está asociado a cero o más pedidos; sin embargo, las consultas deben ser formuladas de una forma que no puede aprovechar ese conocimiento; en vez de eso, la consulta debe realizar un encuentro explícito para navegar por esa asociación.
Además de los problemas mencionados anteriormente, ambas consultas presentan otro problema interesante: devuelven información sobre los empleados y vendedores, respectivamente. Sin embargo, no se puede preguntar al sistema por un “empleado” o un “vendedor”. El sistema no tiene el conocimiento de qué eso significa. Todos los valores devueltos por las consultas son simplemente proyecciones que copian algunos de los valores en las filas de la tabla al conjunto de resultados, perdiéndose cualquier relación con el origen de los datos. Esto significa que no hay una comprensión común a lo largo del código de la aplicación de los conceptos principales de la aplicación, como empleado, ni se pueden hacer cumplir de forma adecuada las restricciones asociadas con ese concepto. Más aún, como los resultados son simplemente proyecciones, la información original que describe de dónde los datos provienen se pierde, exigiendo a los desarrolladores indicar explícitamente al sistema cómo deben realizarse las inserciones, actualizaciones y borrados utilizando sentencias SQL.
Los asuntos que hemos discutido caen en dos categorías:
-
Los relacionados con el hecho de que el modelo lógico (relacional) y su infraestructura asociada no pueden aprovechar el conocimiento del dominio conceptual del modelo de datos de la aplicación, y por lo tanto no son capaces de comprender las entidades de negocio, las relaciones entre ellas ni sus restricciones.
-
Los relacionados con el problema práctico de que las bases de datos tienen esquemas lógicos que generalmente no casan con las necesidades de las aplicaciones; esos esquemas frecuentemente no pueden ser adaptados porque son compartidos por muchas aplicaciones o debido a requisitos no funcionales como las operaciones, propietario de los datos, rendimiento o seguridad.
Los asuntos antes descritos son muy comunes en la mayoría de las aplicaciones corporativas centradas en datos. Para resolver tales problemas, ADO.NET introduce el Marco de Entidades, que consiste en un modelo de datos y un conjunto de servicios de tiempo de diseño y tiempo de ejecución que permiten a los desarrolladores describir los datos de las aplicaciones e interactuar con ellos a un nivel de abstracción “conceptual” apropiado para las aplicaciones de negocio, y que ayuda a aislar a la aplicación de los esquemas lógicos de bases de datos subyacentes.
2.1 Modelado de datos al nivel conceptual: El Modelo de Datos de Entidades
Para resolver el primero de los problemas identificados en la sección anterior, lo que necesitamos es una vía de describir la estructura de los datos (el esquema) que utilice construcciones de más alto nivel.
El Modelo de Datos de Entidades (Entity Data Model – EDM) es un modelo de datos entidad-relación. Los conceptos claves que introduce EDM son:
-
Entidad: las entidades son instancias de Tipos de Entidades (por ejemplo Empleado, Pedido), que son registros ricamente estructurados con una clave. Las entidades se agrupan en Conjuntos de Entidades.
-
Relación: las relaciones asocian entidades, y son instancias de Tipos de Relación (por ejemplo, Pedido tomado-por Vendedor). Las relaciones se agrupan en Conjuntos de Relaciones.
La introducción de un concepto explícito de Entidad y Relación permite a los desarrolladores ser mucho más explícitos al describir esquemas. Además de esos conceptos principales, EDM soporta varias construcciones que extienden aún más su expresividad. Por ejemplo:
-
Herencia: los tipos de entidades pueden ser definidos de forma que hereden de otros tipos (por ejemplo, Empleado podría heredar de Contacto). Esta clase de herencia es estrictamente estructural, lo que significa que no hay “comportamiento” heredado, como ocurre en los lenguajes de programación orientados a objetos. Lo que se hereda es la estructura del tipo de entidad base; además de heredar su estructura, las instancias del tipo de entidad derivado satisfacen la relación “es un” al ser comparados con el tipo de entidad base.
-
Tipos complejos: además de los tipos escalares comunes soportados por la mayor parte de las bases de datos, EDM soporta la definición de tipos complejos y su uso como miembros de los tipos de entidades. Por ejemplo, podríamos definir un tipo complejo Dirección, que tiene propiedades Calle, Número, Ciudad y Estado, y entonces añadir una propiedad de tipo Dirección a un tipo de entidad Contacto.
Con todas estas nuevas herramientas, podríamos redefinir el esquema lógico que hemos utilizado en la sección anterior usando un modelo conceptual:
Figura 2
En lenguaje natural, este esquema tiene los siguientes elementos:
-
Tres tipos de entidades: Vendedor (SalesPerson), Pedido (SalesOrder) y PedidoEnTienda (StoreSalesOrder). Observe que un PedidoEnTienda “es un” Pedido (hereda de SalesOrder), con la característica especial de tener información de impuestos.
-
Una relación entre los tipos de entidades Pedido y Vendedor.
-
Dos conjuntos de entidades: Pedidos y Vendedores; observe que el conjunto de entidades Pedidos puede tener instancias de los tipos de entidades Pedido y PedidoEnTienda.
Este nuevo modelo es mucho más cercano a la vista que una aplicación de ventas utilizaría para su almacén. Entre unos pocos detalles importantes que señalar se incluye el hecho de que los vendedores ya no están dispersos a través de múltiples tablas, sino que son un único conjunto de entidades; además, no hay claves primarias/foráneas en el esquema; en vez de eso, se declara explícitamente en el modelo que existe una relación.
Para ofrecer un ejemplo concreto, en la sección anterior necesitamos un encuentro de tres tablas para acceder a los nombres de los vendedores en una consulta, algo como:
SELECT sp.FirstName, sp.LastName, sp.HireDate FROM SalesPerson sp INNER JOIN Employee e ON sp.SalesPersonID = e.EmployeeID INNER JOIN Contact c ON e.EmployeeID = c.ContactID WHERE e.SalariedFlag = 1 AND e.HireDate >= '2006-01-01'
Ahora que tenemos un modelo EDM de más alto nivel, podríamos escribir esta misma consulta contra un conjunto de entidades SalesPeople de la siguiente forma:
SELECT sp.FirstName, sp.LastName, sp.HireDate FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp WHERE e.HireDate >= '2006-01-01'
Esto es significativamente más simple y tiene exactamente la misma semántica, con la ventaja adicional de que la información sobre cómo crear la vista apropiada de los datos para esta aplicación se expresa ahora de manera declarativa en un artefacto externo (el esquema y mapeado EDM que discutiremos más adelante).
ADO.NET incluye herramientas visuales que se utilizan para diseñar estos esquemas. La salida que producen estas herramientas es un fichero XML que describe el esquema conceptual utilizando el Lenguaje de Descripción de Esquemas (Schema Description Language – SDL). Vea en la sección 6.1 “Esquema EDM de ejemplo representado como XML” del apéndice la versión XML del esquema EDM anterior.
Ahora bien, si este nuevo esquema conceptual es diferente del esquema lógico en la base de datos real, ¿cómo el sistema sabe cómo ir de un lado a otro entre esquemas? La respuesta es “mapeado”.
Para más información sobre el Modelo de Datos de Entidades, vea la referencia [EDM] al final de este documento.
2.2 Llevando los datos al modelo EDM: mapeado
EDM es un modelo conceptual de datos que puede ser utilizado para modelar los datos de un dominio dado. Sin embargo, en algún momento los datos deben ser almacenados en la base de datos real, generalmente una base de datos relacional.
Para ofrecer un mecanismo para almacenar datos modelados mediante EDM en bases de datos relacionales, el Marco de Entidades de ADO.NET alberga una potente infraestructura de vistas cliente diseñadas para gestionar las transformaciones entre el esquema lógico de la base de datos presente en el almacén relacional y el esquema conceptual EDM utilizado por la aplicación.
Además del esquema EDM, el sistema recibe como entrada una especificación de mapeado; esta especificación de mapeado es generada por las herramientas de mapeado y es también un fichero XML.
Para continuar con el ejemplo, si deseáramos mapear el esquema lógico de la base de datos que hemos usado al principio de esta sección al esquema conceptual EDM de la sección anterior, haríamos algo como esto:
Figura 3
Cuando la herramienta de mapeado es utilizada para crear un mapeado del esquema conceptual al lógico, produce un fichero XML que puede ser consumido por los componentes de tiempo de ejecución del Marco de Entidades de ADO.NET. El apéndice incluye la representación XML del mapeado mostrado en la sección 6.2 “Mapeado de ejemplo representado como XML”. Afortunadamente, la herramientas hacen innecesario para la amplia mayoría de los usuarios tener que comprender o tratar con esos ficheros XML.
Además de ofrecer soporte para representar esquemas como esquemas EDM, la infraestructura de vistas cliente en ADO.NET ofrece otras ventajas. Al principio de esta sección hemos discutido cómo las bases de datos con esquemas poseídos por el desarrollador de aplicaciones pueden introducir complejidad en el código de la aplicación. Al utilizar vistas cliente, la complejidad incorporada por los esquemas lógicos externos pueden ser detenidos antes de que alcancen el código de la aplicación; en vez de eso, las vistas pueden ser diseñadas para realizar cualquier re-modelado requerido para los datos que la aplicación consume. De este modo, la aplicación tiene una nueva vista de los datos que tiene sentido para el espacio de problemas al que se dirige. Esto es útil independientemente de que se utilicen nuevas construcciones EDM en el modelo resultante.
Una pregunta obvia en este punto sería por qué no utilizar las vistas de bases de datos tradicionales para esto. Aunque las vistas de bases de datos pueden abstraer bastante de los mapeados, frecuentemente esa solución no sirve por varias razones, tanto de procesamiento como funcionales: (a) muchas de las vistas son simplemente demasiado complejas para ser generadas y mantenidas por los desarrolladores de una manera efectiva en cuanto a costes, incluso en casos de mapeados conceptuales a lógicos simples, (b) las clases de vistas que tienen la propiedad de ser automáticamente actualizables en el almacén son limitadas, y (c) las bases de datos para los principales sistemas de las empresas medianas y grandes son utilizadas por muchas aplicaciones centrales y departamentales, y hacer que cada aplicación individual cree varias vistas en la base de datos ensuciaría el esquema de la base de datos y crearía unos costes de mantenimiento significativos para los administradores de bases de datos. Adicionalmente, las vistas de bases de datos están limitadas a la expresividad del modelo relacional, y generalmente carecen de algunos de los conceptos del mundo real que contiene EDM, como la herencia y los tipos complejos.
Las vistas cliente de ADO.NET funcionan completamente en el cliente, por lo que cada desarrollador de aplicaciones puede crear vistas que adapten los datos a un formato que tenga sentido para cada aplicación particular sin afectar a la base de datos real u otras aplicaciones. El espectro de vistas actualizables soportadas por el Marco de Entidades es mucho más amplio que el soportado por cualquier almacén relacional.
2.3 Exponiendo EDM y mapeando a la API de ADO.NET: el proveedor de mapeado
Los conceptos de EDM y de mapeado parecen inicialmente bastante abstractos, por lo que en este punto uno podría preguntarse cómo se expresan concretamente en la API de ADO.NET.
Hemos decidido introducir un nuevo proveedor de acceso a datos para ADO.NET llamado “proveedor de mapeado” (proveedor de mapeado). Del mismo modo que un proveedor tradicional se conecta a un almacén y suministra a la aplicación una vista de los datos del almacén en su esquema lógico, el proveedor de mapeado se conecta a un modelo conceptual EDM y ofrece a la aplicación una vista conceptual de los datos.
Al proveedor de mapeado se le entrega el esquema EDM y la información de mapeado, de modo que pueda utilizar internamente la infraestructura de mapeado para traducir entre los esquemas lógico y conceptual.De modo que, por ejemplo, para ejecutar una consulta contra el modelo EDM que encuentra los nombres y fechas de contratación de los vendedores después de una fecha dada, el código ADO.NET sería:
using(MapConnection con = new
MapConnection(Settings.Default.AdventureWorks)) {
con.Open();
MapCommand cmd = con.CreateCommand();
cmd.CommandText =
"SELECT sp.FirstName, sp.LastName, sp.HireDate " +
"FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp " +
"WHERE sp.HireDate > @date";
cmd.Parameters.AddWithValue("date", hireDate);
DbDataReader r = cmd.ExecuteReader();
while(r.Read()) {
Console.WriteLine("{0}\t{1}", r["FirstName"], r["LastName"]);
}
}
Observe que el patrón debe ser muy familiar para los desarrolladores ADO.NET; se parece mucho al código para ADO.NET 2.0, con la única diferencia de que utiliza un proveedor diferente.
Por detrás del telón, el proveedor de mapeado utilizará el esquema EDM y la información de mapeado para traducir desde y hacia el modelo conceptual. Y entonces utilizará un proveedor de ADO.NET regular para hablar con la base de datos subyacente (por ejemplo, utilizará System.Data.SqlClient para hablar con una base de datos SQL Server).
2.4 Consultas contra un modelo EDM: Entity SQL
Cuando una aplicación utiliza un modelo EDM y el proveedor de mapeado para acceder a él, deja de conectarse directamente a una base de datos o ver cualquier construcción específica de la base de datos; toda la aplicación opera en términos del modelo EDM de más alto nivel.
Esto significa que no se puede utilizar el lenguaje de consultas nativo de la base de datos; no sólo la base de datos no comprenderá el modelo EDM, sino que además los lenguajes de consulta de bases de datos actuales no ofrecen las construcciones requeridas para trabajar con los elementos introducidos por EDM, tales como la herencia, las relaciones, los tipos complejos, etc.
Para hacer posibles las consultas contra modelos EDM, el Marco de Entidades de ADO.NET introduce un lenguaje de consultas diseñado para trabajar con EDM y que puede aprovechar toda expresividad del modelo de datos de entidades. Ese lenguaje se llama Entity SQL y debe ser familiar a todos los desarrolladores que han utilizado anteriormente algún dialecto de SQL. Entity SQL suministra al Marco de Entidades una capacidad de consulta dinámica, donde las consultas pueden ser formuladas estáticamente en tiempo de diseño o creadas dinámicamente en el contexto de aplicaciones con enlace tardío.
Por ejemplo, esta es una consulta Entity SQL válida para el ejemplo anterior:
SELECT sp.FirstName, sp.LastName, sp.HireDate FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp WHERE sp.HireDate > @date
La estructura general de una consulta de Entity SQL es la secuencia usual SELECT-FROM-WHERE presente en el SQL tradicional. Además de estos elementos básicos, Entity SQL introduce varios conceptos para permitir a los desarrolladores aprovechar la expresividad de los modelos EDM conceptuales; la siguiente es una descripción de los principales conceptos adicionales introducidos por Entity SQL:
Tratamiento de entidades. Los esquemas EDM conceptuales se diseñan alrededor de las entidades. Los conceptos de negocio se reflejan directamente en tipos de entidades EDM cuyas instancias son almacenadas en conjuntos de entidades. Del mismo modo que las consultas en el mundo relacional se formulan contra tablas, las consultas en el mundo EDM se formulan contra conjuntos de entidades. Así que el punto inicial para una consulta es un grupo de entidades que provienen de uno o más conjuntos de entidades.
Dependiendo de sus necesidades en cada escenario particular, usted podrá elegir entre proyectar valores individuales o preservar la entidad completa. Preservar la entidad completa es interesante cuando se desea que el sistema nos asista con servicios construidos alrededor de las entidades. Por ejemplo, gracias a los metadatos suministrados en el esquema EDM y la especificación de mapeado, el Marco de Entidades de ADO.NET sabe cómo reflejar las actualizaciones a las entidades en el almacén sin que el usuario tenga que suministrar sentencias INSERT, UPDATE y DELETE, como ha requerido tradicionalmente el adaptador de datos de ADO.NET.
Las consultas con proyección lucen muy similares a las consultas SQL tradicionales, con la diferencia de que los alias de tablas son obligatorios:
SELECT sp.FirstName, sp.LastName, sp.HireDate FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp WHERE sp.HireDate > @date
Si en este ejemplo quisiéramos proyectar las entidades de los vendedores, escribiríamos:
SELECT VALUE sp FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp WHERE sp.HireDate > @date
El modificador “VALUE” indica al sistema que debe producir en el conjunto de resultados un conjunto de valores que representan instancias de entidades o escalares; los resultados preservarán todos los metadatos requeridos para describir completamente los valores, incluyendo cómo extraer las claves primarias para las entidades, de dónde proviene la entidad, etc.
Desde la perspectiva del conjunto de resultados, cuando una entidad es proyectada, el objeto DataReader resultante tendrá una columna para cada miembro del nivel superior del tipo de entidad (por lo que, efectivamente, parece como si todas las columnas fueran proyectadas en la consulta, pero con la diferencia de que hay metadatos adicionales disponibles sobre la entidad).
Navegación de relaciones. Además de las entidades, el otro elemento clave de EDM es el concepto explícito de asociación a través de relaciones; dado que el sistema conoce las relaciones entre entidades, el lenguaje de consultas puede ser utilizado para navegar explícitamente por esas relaciones sin tener que usar construcciones como los encuentros.
Por ejemplo, anteriormente hemos usado la siguiente consulta para encontrar los vendedores con pedidos por más de un cierto valor:
SELECT SalesPersonID, FirstName, LastName, HireDate FROM SalesPerson sp INNER JOIN Employee e ON sp.SalesPersonID = e.EmployeeID INNER JOIN Contact c ON e.EmployeeID = c.ContactID INNER JOIN SalesOrder o ON sp.SalesPersonID = o.SalesPersonID WHERE e.SalariedFlag = 1 AND o.TotalDue > 200000
La complejidad de esta consulta viene dada por 1) el hecho de que la información sobre los vendedores está diseminada por varias tablas y 2) la navegación de los vendedores a los pedidos debe ser realizada de manera indirecta, a través de un encuentro.
Utilizando el nuevo esquema conceptual EDM que hemos definido antes, podemos formular la consulta de la siguiente forma:
SELECT VALUE sp FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp WHERE EXISTS( SELECT VALUE o FROM NAVIGATE(p, AdventureWorks.SalesPerson_Order) AS o WHERE o.TotalDue > 200000)
El operador “NAVIGATE” permite que una consulta navegue explícitamente una relación; la aplicación no necesita saber cómo se mantiene la relación o utilizar métodos indirectos como encuentros para utilizarla.
Soporte de herencia. Debido a que EDM soporta la herencia de los tipos de entidades, el lenguaje de consultas debe permitir a los usuarios formular consultas que aprovechen las relaciones de jerarquía de tipos mediante herencia.
Por ejemplo, nuestro esquema conceptual EDM ofrece un tipo de entidad SalesOrder (Pedido) y un subtipo StoreSalesOrder (PedidoEnTienda) que tiene algunas características específicas. Como cada StoreSalesOrder “es un” SalesOrder, una consulta contra el conjunto de entidades SalesOrders devolverá un conjunto de resultados polimórfico que contendrá instancias tanto del tipo SalesOrder como de StoreSalesOrder:
SELECT VALUE o FROM AdventureWorks.AdventureWorksDB.SalesOrders AS o
Si deseáramos solamente los pedidos provenientes de tiendas, lo preguntaríamos al sistema explícitamente aprovechando la jerarquía de tipos:
SELECT VALUE o FROM AdventureWorks.AdventureWorksDB.SalesOrders AS o WHERE o IS OF (AdventureWorks.StoreSalesOrder)
En esta consulta, el operador “IS OF” comprueba si una expresión (“o” en este caso) es una instancia del tipo especificado entre paréntesis.
Lecciones aprendidas. Además de las diferentes extensiones en Entity SQL diseñadas para ofrecer una experiencia de consultas de primera clase contra esquemas EDM, Entity SQL incorpora varias mejoras que provienen de la experiencia con dialectos SQL más tradicionales.
Por ejemplo, en Entity SQL las expresiones pueden devolver escalares o colecciones, y las colecciones son construcciones de primera clase que pueden aparecer en la mayoría de los contextos en que se admite una expresión, pudiendo ser combinadas. Por ejemplo, se puede utilizar una colección en la cláusula FROM para que actúe como origen para una consulta, o en la lista SELECT, lo que hará que una de las columnas del conjunto de resultados sea una colección en vez de un escalar.
Para una descripción en profundidad de Entity SQL, vea la referencia [eSQL] al final de este documento.
Estpales:
Marco de Entidades de ADO.NET: Servicios de Objetos
La gran mayoría del nuevo código para las aplicaciones de negocios se escribe en lenguajes de propósito general y orientados a objetos, como Visual Basic y C#. Estos lenguajes de programación y sus entornos de desarrollo modelan las entidades de negocio como clases y el comportamiento de éstas como código. A diferencia de lo anterior, ADO.NET hasta el momento ha expuesto los datos provenientes de bases de datos como “valores”, o sea, como filas y columnas. Para interactuar con las bases de datos, las aplicaciones tienen que salvar el desajuste de impedancias existente entre los datos y el código de la aplicación; esto incluye tanto la manera en que se formulan las consultas como la manera en que se exponen los resultados.
El Marco de Entidades de ADO.NET incluye una capa de servicios de objetos que reduce, y frecuentemente elimina, este desajuste de impedancias.
3.1 Los mismos datos, pero como objetos
Las aplicaciones, y en particular las aplicaciones grandes o los sistemas compuestos por varias aplicaciones, rara vez utilizan una única representación de los datos a través de toda su base de código; varios aspectos, como el conocimiento estático o dinámico de la estructura de las consultas y el conjunto de resultados, el modelo de interacción del usuario y la aplicación, etc. afectan la manera en que las aplicaciones deben interactuar con los datos en las bases de datos.
En lugar de introducir una infraestructura totalmente nueva e independiente para exponer los datos de la base de datos como objetos, el Marco de Entidades de ADO.NET incluye una capa de “servicios de objetos” que se integra con el resto de la pila y expone los valores de entidades como objetos .NET como una opción de presentación.
Independientemente de si usted elige consumir sus datos como “valores” (filas y columnas) o como objetos, utilizará la misma infraestructura; o sea, que se puede utilizar el mismo esquema conceptual EDM, conjuntamente con el mismo mapeado y el mismo lenguaje de consultas – Entity SQL.
Por ejemplo, el siguiente fragmento de código obtiene un subconjunto de los vendedores en el sistema y manipula los resultados utilizando un DataReader normal (esto es, utilizando “valores”):
using(MapConnection con = new
MapConnection(Settings.Default.AdventureWorks)) {
con.Open();
MapCommand cmd = con.CreateCommand();
cmd.CommandText =
"SELECT VALUE sp " +
"FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp " +
"WHERE sp.HireDate > @date";
cmd.Parameters.AddWithValue("date", hireDate);
DbDataReader r = cmd.ExecuteReader();
while(r.Read()) {
Console.WriteLine("{0}\t{1}", r["FirstName"], r["LastName"]);
}
}
Aunque esto es adecuado para escenarios de enlace tardío como la generación de informes y la minería de datos, o para seriar resultados directamente, por ejemplo, de un servicio Web, en los casos en los que es necesario escribir una lógica de negocio “pesada” es generalmente mucho mejor representar las entidades de negocio mediante objetos. La versión basada en objetos del código anterior, utilizando la próxima versión de ADO.NET, lucirá de la siguiente manera:
using(MapConnection con = new
MapConnection(Settings.Default.AdventureWorks)) {
con.Open();
ObjectContext ctx = new ObjectContext(con);
Query<SalesPerson> newSalesPeople = ctx.GetQuery<SalesPerson>(
"SELECT VALUE sp " +
"FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp " +
"WHERE sp.HireDate > @date",
new QueryParameter("@date", hireDate));
foreach(SalesPerson p in newSalesPeople) {
Console.WriteLine("{0}\t{1}", p.FirstName, p.LastName);
}
}
En lugar de utilizar un objeto de comando para representar la consulta, se utiliza un “contexto de objetos” que actúa como punto de entrada a los servicios de objetos y un objeto Query que representa una consulta en el espacio de objetos. Observe que aún se utiliza la misma conexión de mapeado (que apunta al mismo esquema EDM y mapeado) así como el mismo lenguaje de consultas. La única diferencia es que ahora los resultados se devuelven como objetos.
Una pregunta que surge inmediatamente de este ejemplo es: ¿de dónde sale el tipo “SalesPerson”? El Marco de Entidades de ADO.NET incluye una herramienta que, dado un esquema EDM, generará las clases .NET que representan a las entidades EDM dentro del entorno .NET. Las clases generadas son clases parciales, por lo que pueden ser extendidas mediante lógica de negocio personalizada situada en ficheros separados, para no interferir con el generador de código.
Si la herramienta tiene acceso a todo el esquema EDM, no solo tiene acceso a la definición de los tipos de entidades, sino también de los conjuntos de entidades, las relaciones, etc. En base a esa información, la herramienta no solo generará clases para cada tipo de entidad, sino también una clase de alto nivel que representa al almacén como un todo, tal como es visto desde la perspectiva de EDM, con sus conjuntos de entidades, relaciones, etc. Esto simplifica aún más la escritura de código de acceso a datos que utiliza esos objetos. El mismo ejemplo mostrado antes, cuando se utiliza la versión fuertemente tipada del contexto de objetos generado por la herramienta, se convierte en:
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) {
Query<SalesPerson> newSalesPeople = aw.GetQuery<SalesPerson>(
"SELECT VALUE sp " +
"FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp " +
"WHERE sp.HireDate > @date",
new QueryParameter("@date", hireDate));
foreach(SalesPerson p in newSalesPeople) {
Console.WriteLine("{0}\t{1}", p.FirstName, p.LastName);
}
}
Como puede observarse, la mayoría del código de fontanería ha sido eliminado, y solo queda el código que representa la intención de la aplicación. En este punto, la mayor parte del problema de desajuste de impedancias mencionado anteriormente ha sido eliminado. Este es un objetivo clave del Marco de Entidades de ADO.NET.
3.2 Conceptos EDM en el espacio de objetos
En la sección 2.1 se presentaron varios conceptos de EDM como las entidades, las relaciones y la herencia. En general, todos los conceptos de EDM son mapeados por los servicios de objetos al entorno .NET.Las entidades son simplemente mapeadas a clases que satisfacen un contrato específico. Para hacer las cosas simples, estas clases son generadas automáticamente a partir del esquema EDM por una herramienta.
Las relaciones se exponen en el espacio de objetos como propiedades que pueden ser navegadas directamente. Por ejemplo, nuestro modelo EDM de AdventureWorks define una relación entre los tipos de entidades SalesPerson y SalesOrder. En base a esa información, la herramienta de generación de código creará los miembros apropiados de forma que el siguiente código funcione adecuadamente:
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) {
Query<SalesPerson> newSalesPeople = aw.GetQuery<SalesPerson>(
"SELECT VALUE sp " +
"FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp " +
"WHERE sp.HireDate > @date",
new QueryParameter("@date", hireDate));
foreach(SalesPerson p in newSalesPeople) {
// para cada vendedor que cumple cierta condición, procesar
// sus pedidos pendientes (suponiendo que ‘pendiente’ es status == 0)
if(NeedsOrderProcessing(p)) {
// consulta de los pedidos pendientes
foreach(SalesOrder o in p.Orders.Source.Where("it.Status == 0")) {
// procesar pedido
ProcessOrder(o);
}
}
}
}
Observe que la navegación de la relación se produce simplemente haciendo referencia a una propiedad de una de las entidades involucradas en la relación.
Otra manera interesante de ilustrar cómo los elementos de EDM son expuestos por la capa de servicios de objetos es la herencia. El esquema EDM que hemos estado utilizando incluye un conjunto de entidades SalesOrders que contiene instancias de las clases SalesOrder y StoreSalesOrder; esto se representa directamente en el ambiente .NET como herencia en la jerarquía de clases. La herramienta de generación de código creará una clase StoreSalesOrder que hereda de la clase SalesOrder, y las instancias se materializarán apropiadamente.
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) {
Query<SalesOrder> pendingOrders = aw.GetQuery<SalesOrder>(
"SELECT VALUE " +
"FROM AdventureWorks.AdventureWorksDB.SalesOrders AS o " +
"WHERE o.Status = 0");
foreach(SalesOrder o in pendingOrders) {
// utilizar el tipo de tiempo de ejecución para determinar
// cómo procesar este pedido
if(o is StoreSalesOrder) {
ValidateTaxByState((StoreSalesOrder)o);
ProcessLocalOrder(o);
}
else {
ProcessOnlineOrder(0);
}
}
}
3.3 Manipulación de datos y persistencia de cambios
Usualmente, cuando una aplicación desea hacer cambios en una base de datos debe emitir sentencias INSERT, UPDATE y DELETE contra ella. En la práctica, las aplicaciones tienden a tener varias “entidades” (incluso definidas informal o implícitamente), y los desarrolladores deben escribir (o utilizar herramientas que las generen) sentencias de DML de SQL para cada una de las “entidades” en el sistema. Esta es una tarea tediosa y propensa a errores, que genera una gran cantidad de código que requerirá mantenimiento en el futuro.
El Marco de Entidades de ADO.NET incluye suficientes metadatos sobre las entidades en el sistema, de modo que puede reflejar los cambios en las entidades a la base de datos sin necesidad de que el usuario suministre las sentencias SQL para ello. Por ejemplo:
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) {
// buscar todos los vendedores contratados hace 5 años o más
Query<SalesPerson> oldSalesPeople = aw.GetQuery<SalesPerson>(
"SELECT VALUE sp " +
"FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp " +
"WHERE sp.HireDate < @date",
new QueryParameter("@date", DateTime.Today.AddYears(-5)));
foreach(SalesPerson p in oldSalesPeople) {
// llamar al sistema de Recursos Humanos a través de un servicio
// Web para ver si este vendedor se merece una promoción
// (observe que este tipo de entidad es XML-seriable)
if(HRWebService.ReadyForPromotion(p)) {
p.Bonus += 10; // subirle el bono un 10%
p.Title = "Senior Sales Representative"; // darle una promoción
}
}
// enviar los cambios a la base de datos
aw.SaveChanges();
}
Las actualizaciones son sencillas porque el Marco de Entidades hace varias cosas por detrás del telón para simplificarle la vida al desarrollador. En particular, el sistema:
-
Hace un seguimiento de cada objeto que ha sido devuelto por una consulta. El contexto de objetos, ya sea una instancia regular de ObjectContext o una instancia tipada como AdventureWorksDB, actúa como un ámbito lógico dentro del cual se hace un seguimiento de las instancias.
-
Mantiene las versiones originales de los valores utilizados para materializar cada objeto. Esto permite al sistema realizar comprobaciones de concurrencia optimista contra la base de datos durante las actualizaciones.
-
Hace un seguimiento de qué objetos han cambiado, para que cuando se llame a SaveChanges ADO.NET sepa qué entidades deben ser actualizadas en el almacén.
-
Mantiene información de metadatos de los conjuntos de resultados que describen de qué conjuntos de entidades esas entidades provienen, cuál es su tipo de entidad, etc. Toda esta información permite al sistema generar las sentencias requeridas para las actualizaciones sin necesidad de que el usuario tenga que suministrar ninguna información.
-
Transforma los cambios a nivel de objetos en actualizaciones conceptuales y lógicas.
Las actualizaciones no solo operan sobre los resultados inmediatos de una consulta, sino que también operan a través de las colecciones que representan las relaciones entre entidades. Por ejemplo, añadir un nuevo pedido y asociarlo a un vendedor dado es un tipo de cambio, y se implementaría simplemente añadiendo un nuevo objeto SalesOrder a la colección Orders del vendedor y entonces ejecutando la actualización.
LINQ to Entities: Consultas integradas en los lenguajes
A pesar de los grandes avances en la integración de bases de datos y entornos de desarrollo, aún existe un desajuste de impedancias entre ambos que no se resuelve fácilmente solo mejorando las librerías y API utilizadas para la programación de datos. Aunque el Marco de Entidades elimina casi completamente el desajuste de impedancias entre las filas lógicas y los objetos, la integración del Marco de Entidades con extensiones a los lenguajes de programación existentes para expresar de una manera natural las consultas dentro del propio lenguaje ayuda a eliminarlo completamente.
Más específicamente, la mayoría de los desarrolladores de aplicaciones de negocios de hoy deben tratar con al menos dos lenguajes de programación: el lenguaje utilizado para modelar la lógica de negocio y la capa de presentación –generalmente un lenguaje orientado a objetos de alto nivel como C# o Visual Basic- y el lenguaje utilizado para interactuar con la base de datos –que generalmente es algún dialecto de SQL.
Esto no solo significa que los desarrolladores deben dominar varios lenguajes para desarrollar aplicaciones de manera efectiva, sino que a la vez introduce lagunas a través del código de la aplicación cada vez que se salta de un entorno a otro. Por ejemplo, en la mayoría de los casos las aplicaciones ejecutan consultas contra bases de datos utilizando una API de acceso a datos como ADO.NET y especificando la consulta entre comillas dentro del programa; dado que la consulta es un mero literal de cadena para el compilador, éste no comprueba si la sintaxis es apropiada, ni la valida para asegurarse de que hace referencia a elementos existentes como nombres de tablas y columnas.
Resolver este problema es una de los temas principales de la próxima versión de los lenguajes de programación de Microsoft, C# y Visual Basic.
4.1 Consultas integradas en los lenguajes
La próxima generación de los lenguajes C# y Visual Basic incluyen diversas innovaciones que facilitan la manipulación de datos por el código de la aplicación. El Proyecto LINQ consiste en un conjunto de extensiones a esos lenguajes y sus librerías de soporte que permiten a los usuarios formular consultas dentro del propio lenguaje de programación, sin tener que hacer uso de otro lenguaje embebido en los programas como literales de cadena y que no puede ser comprendido o verificado durante la compilación.
Las consultas formuladas mediante LINQ pueden ejecutarse contra diferentes fuentes de datos, tales como estructuras en memoria, documentos XML y, a través de ADO.NET, contra bases de datos, modelos de entidades e instancias de la clase DataSet. Aunque en algunos de estos casos se utilizan por detrás del telón diferentes implementaciones, todas ellas exponen la misma sintaxis y construcciones del lenguaje.
Los detalles concretos de la sintaxis para las consultas son específicos para cada lenguaje, y se mantienen los mismos a través de diferentes fuentes de datos LINQ. Por ejemplo, la siguiente es una consulta en Visual Basic que opera sobre un array en memoria:
Dim numbers() As Integer = {5, 7, 1, 4, 9, 3, 2, 6, 8}
Dim smallnumbers = From n In numbers _
Where n <= 5 _
Select n _
Order By n
For Each Dim n In smallnumbers
Console.WriteLine(n)
Next
Esta es la versión C# de la misma consulta:
int[] numbers = new int[] {5, 7, 1, 4, 9, 3, 2, 6, 8};
var smallnumbers = from n in numbers
where n <= 5
orderby n
select n;
foreach(var n in smallnumbers) {
Console.WriteLine(n);
}
Las consultas contra fuentes de datos tales como modelos de entidades y objetos DataSet lucen iguales sintácticamente, como podrá comprobar en las siguientes secciones.
Para más información sobre el Proyecto LINQ, vea la referencia [LINQ] al final de este documento.
4.2 LINQ y el Marco de Entidades de ADO.NET
Como se ha discutido en la sección sobre los servicios de objetos del Marco de Entidades de ADO.NET, la próxima versión de ADO.NET incluye una capa que permite exponer los datos de la base de datos como objetos .NET. Más aún, las herramientas de ADO.NET generan clases .NET que representan el esquema EDM en el ambiente .NET. Esto hace de la capa de objetos un destino ideal para el soporte LINQ, permitiendo a los desarrolladores formular consultas contra una base de datos directamente desde el lenguaje de programación utilizado para crear la lógica de negocio. Esta capacidad se conoce como LINQ to Entities.
Por ejemplo, anteriormente en el documento se ha presentado este fragmento de código que consultaría una base de datos para recuperar objetos:
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) {
Query<SalesPerson> newSalesPeople = aw.GetQuery<SalesPerson>(
"SELECT VALUE sp " +
"FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp " +
"WHERE sp.HireDate > @date",
new QueryParameter("@date", hireDate));
foreach(SalesPerson p in newSalesPeople) {
Console.WriteLine("{0}\t{1}", p.FirstName, p.LastName);
}
}
Aprovechando los tipos generados automáticamente por la herramienta de generación, más el soporte LINQ en ADO.NET, podemos rescribir el código así:
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) {
var newSalesPeople = from p in aw.SalesPeople
where p.HireDate > hireDate
select p;
foreach(SalesPerson p in newSalesPeople) {
Console.WriteLine("{0}\t{1}", p.FirstName, p.LastName);
}
}
O, en sintaxis Visual Basic:
Using aw As New AdventureWorksDB(Settings.Default.AdventureWorks)
Dim newSalesPeople = From p In aw.SalesPeople _
Where p.HireDate > hireDate _
Select p
For Each p As SalesPerson In newSalesPeople
Console.WriteLine("{0} {1}", p.FirstName, p.LastName)
Next
End Using
Esta consulta LINQ será procesada por el compilador, lo que significa que obtendremos validación en tiempo de compilación como lo tendría el código de la aplicación. Los errores sintácticos, así como los errores en los nombres de miembros y tipos de datos serán cacheados por el compilador y reportados en tiempo de compilación, en lugar de los errores usuales de tiempo de ejecución que son tan comunes durante el desarrollo con SQL y un lenguaje de programación huésped.
Los resultados de estas consultas son objetos que representan entidades ADO.NET, así que es posible manipularlos y actualizarlos utilizando los mismos medios que están disponibles al utilizar Entity SQL para la formulación de consultas.
Aunque este ejemplo muestra una consulta muy simple, las consultas LINQ pueden ser muy expresivas e incluir ordenación, agrupación, encuentros, proyección, etc. Las consultas pueden producir resultados “planos” o compuestos por objetos complejos fabricados mediante expresiones normales de C# o Visual Basic para producir cada fila.
Por ejemplo, la siguiente es una variación del código anterior que ordena los resultados por fecha de contratación y luego por nombre, y produce solo unos cuantos miembros en vez de las entidades completas:
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) {
var newSalesPeople = from p in aw.SalesPeople
where p.HireDate > hireDate
orderby p.HireDate, p.FirstName
select new { Name = p.FirstName + " " + p.LastName,
HireDate = p.HireDate };
foreach(SalesPerson p in newSalesPeople) {
Console.WriteLine("{0}\t{1}", p.FirstName, p.LastName);
}
}
Además, las relaciones no solo se exponen como propiedades que pueden ser utilizadas para la navegación, como hemos mencionado en la sección anterior, sino que también pueden ser utilizadas en una consulta para realizar una extracción y transformación sofisticada de los datos de una manera bastante declarativa; por ejemplo, para obtener todos los pedidos de los empleados contratados este año:
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) {
var newSalesPeople = from o in aw.SalesOrders
where o.SalesPerson.HireDate >= new DateTime(2006, 1, 1)
select o;
// procesar pedidos
// ...
}
4.3 DataSet finalmente obtiene capacidades completas de consulta: LINQ to DataSet
Uno de los elementos claves del modelo de programación de ADO.NET es la capacidad para cachear explícitamente datos de una manera desconectada y agnóstica con respecto al almacén de datos utilizando instancias de la clase DataSet. Un DataSet representa un conjunto de tablas y relaciones, junto con los metadatos apropiados para describir la estructura y las restricciones de los datos contenidos en él. ADO.NET incluye varias clases que facilitan la carga de datos de una base de datos a un DataSet, y devolver los cambios realizados sobre un DataSet a la base de datos.
Un aspecto interesante de la clase DataSet es que permite a las aplicaciones recuperar un subconjunto de la información contenida en una base de datos hacia el espacio de la aplicación y luego manipularla en memoria manteniendo su forma relacional. Esto hace posibles muchos escenarios que requieren flexibilidad en la manera en que los datos son representados y gestionados. En particular, la generación de informes, el análisis y las aplicaciones de inteligencia de negocio soportan este método de manipulación.
Para consultar los datos almacenados en un DataSet, la API de esta clase incluye métodos, tales como DataTable.Select(), para buscar datos de ciertas maneras predeterminadas. Sin embargo, no ha estado disponible un mecanismo general para ejecutar consultas ricas sobre objetos DataSet que ofrezca la expresividad frecuentemente requerida por las aplicaciones centradas en datos.
LINQ ofrece una oportunidad única para introducir ricas capacidades de consulta sobre la clase DataSet, y de un modo que se integra en el entorno.
La Presentación Preliminar de ADO.NET incluye soporte completo para ejecutar consultas LINQ sobre objetos DataSet tanto regulares como tipados. Para ilustrar esta funcionalidad, supongamos que tenemos un DataSet con dos tablas, “SalesOrderHeader” y “SalesOrderDetail”; a continuación se muestra cómo se obtendrían los códigos y fechas de todos los pedidos realizados en línea:
DataSet ds = new DataSet();
FillOrders(ds); // este método rellena el DataSet de la base de datos
DataTable orders = ds.Tablas["SalesOrderHeader"];
var query = from o in orders.ToQueryable()
where o.Field<bool>("OnlineOrderFlag") == true
select new { SalesOrderID = o.Field<int>("SalesOrderID"),
OrderDate = o.Field<DateTime>("OrderDate") };
foreach(var order in query) {
Console.WriteLine("{0}\t{1:d}", order.SalesOrderID, order.OrderDate);
}
Este es el mismo código utilizando sintaxis de Visual Basic:
Dim ds As New DataSet()
FillOrders(ds)
Dim orders As DataTable = ds.Tablas("SalesOrderHeader")
Dim query = From o In orders.ToQueryable() _
Where o!OnlineOrderFlag = True _
Select o!SalesOrderID, o!OrderDate
For Each Dim o In query
Console.WriteLine("{0} {1:d}", o.SalesOrderID, o.OrderDate)
Next
Una observación interesante es que el soporte de enlace tardío en Visual Basic permite que las consultas contra instancias no tipadas de DataSet escritas en este lenguaje sean más fáciles de leer que sus homólogas en C#. Más adelante discutimos cómo los DataSet tipados resuelven este problema, mejorando significativamente la situación para ambos lenguajes.
Una petición común con relación a la clase DataSet es el soporte para encuentros entre sus miembros DataTable; esto se hace posible con LINQ. A continuación se presenta un ejemplo en el que se cruzan las tablas SalesOrderHeader y SalesOrderDetail:
DataSet ds = new DataSet();
FillOrders(ds);
DataTable orders = ds.Tablas["SalesOrderHeader"];
DataTable details = ds.Tablas["SalesOrderDetail"];
var query = from o in orders.ToQueryable()
join d in details.ToQueryable()
on o.Field<int>("SalesOrderID") equals d.Field<int>("SalesOrderID")
where o.Field<bool>("OnlineOrderFlag") == true
select new { SalesOrderID = o.Field<int>("SalesOrderID"),
OrderDate = o.Field<DateTime>("OrderDate"),
ProductID = d.Field<int>("ProductID"),
Quantity = d.Field<short>("OrderQty") };
foreach(var line in query) {
Console.WriteLine("{0}\t{1:d}\t{2}\t{3}",
line.SalesOrderID, line.OrderDate,
line.ProductID, line.Quantity);
}
Aunque la combinación de LINQ y DataSet resulta en una herramienta muy potente, el código queda algo sobrecargado por varias razones:
-
El acceso a las columnas aún se hace mediante enlace tardío, por lo que los nombres de columnas deben ir entre comillas (esto no solo hace confuso el código, sino que además impide que el compilador pueda comprobar los nombres de columnas en tiempo de compilación).
-
El acceso a campos de un DataSet es no tipado de manera predeterminada (o sea, el resultado de tabla [“columna”] es de tipo Object); una manera de resolver esto sería utilizar una conversión explícita, pero eso no nos protegería de los valores nulos (que se representan como DBNull.Value en DataSet). Para hacer uniforme el acceso a los campos y gestionar los nulos de un modo apropiado, se introduce un nuevo operador, “Field”. Por lo que para acceder a una columna llamada “Fecha” de tipo DateTime se puede utilizar tabla.Field<DateTime>(“Fecha”).
Si el esquema de un DataSet es conocido durante el diseño de la aplicación, el uso de objetos DataSet tipados ofrece una mucho mejor experiencia al utilizar LINQ. Las tablas y tipos de filas en un DataSet tipado incluyen miembros tipados para cada una de las columnas, lo que simplifica en gran medida el acceso; adicionalmente, el propio DataSet ofrece propiedades para un fácil acceso a las diversas tablas almacenadas en él.
Si creamos un DataSet tipado con las mismas tablas utilizadas en el ejemplo anterior, podremos escribir la primera consulta de la siguiente forma:
OrdersDataSet dsOrders = new OrdersDataSet();
FillOrders(ds); // este método rellena el DataSet de la base de datos
var query = from o in dsOrders.SalesOrderHeader
where o.OnlineOrderFlag == true
select new { o.SalesOrderID,
o.OrderDate };
foreach(var order in query) {
Console.WriteLine("{0}\t{1:d}", order.SalesOrderID, order.OrderDate);
}
Como se puede ver, las consultas se hacen mucho más simples. Lo mismo es aplicable al otro ejemplo:
OrdersDataSet ds = new OrdersDataSet();
FillOrders(ds);
var query = from o in dsOrders.SalesOrderHeader
join d in dsOrders.SalesOrderDetail
on o.SalesOrderID equals d.SalesOrderID
where o.OnlineOrderFlag == true
select new { o.SalesOrderID,
o.OrderDate,
d.ProductID,
Quantity = d.OrderQty };
foreach(var line in query) {
Console.WriteLine("{0}\t{1:d}\t{2}\t{3}",
line.SalesOrderID, line.OrderDate,
line.ProductID, line.Quantity);
}
Además de dar soporte a los contratos apropiados para la integración con LINQ, la implementación de LINQ para DataSet es lo suficientemente inteligente como para evaluar ciertas consultas y decidir si su ejecución predeterminada (hecha a través de la implementación de los operadores de consulta estándar) será apropiada para un DataSet concreto. Si hay índices en el objeto DataSet que puedan utilizarse para acelerar la ejecución de los consultas, entonces la estrategia de ejecución se adaptará en tiempo de ejecución para intentar procesar las consultas más eficientemente.
4.4 LINQ to SQL
Para los desarrolladores que no necesitan el mapeado a un modelo conceptual, LINQ to SQL (antes conocido como DLinq) permite aplicar directamente el modelo de programación LINQ sobre el esquema de base de datos existente.
Al igual que el soporte para LINQ sobre las entidades de ADO.NET descrito en la Sección 4.2, LINQ to SQL permite a los desarrolladores generar clases.NET que representan datos. En vez de mapear a un modelo conceptual de datos, estas clases generadas mapean directamente a las tablas, vistas, procedimientos almacenados y funciones definidas por el usuario de la base de datos. Mediante LINQ to SQL, los desarrolladores pueden escribir código directamente contra el esquema de almacenamiento, utilizando el mismo patrón de programación LINQ descrito anteriormente para colecciones en memoria, entidades u objetos DataSet, así como otras fuentes de datos como XML.
Usando las clases generadas de LINQ to SQL, podemos escribir el siguiente código que ahora luce familiar contra la base de datos SQL Server Northwind en una versión local de SQLExpress.
string connectString =
"AttachDBFileName='C:\\ProgramFiles\\LINQ Preview\\Data\\Northwnd.mdf';" +
"Server='.\\SQLEXPRESS';Integrated Security=SSPI;enlist=false";
using(Northwind db = new Northwind(connectString)) {
var customers = from c in db.Customers
where c.City == "London"
select c;
foreach(Customer c in customers) {
Console.WriteLine("{0}\t{1}", c.ContactName, c.CompanyName);
}
}
Como puede observar, este código es muy similar al que escribimos contra una vista conceptual de la base de datos AdventureWorks utilizando LINQ to Entities en la Sección 4.2. La potencia de LINQ estriba en que hace posible aplicar el mismo patrón de programación sencillo contra cualquier tipo de datos.
Referencias
[NGEN] Next-Generation Data Access. ADO.NET Technical Preview, mayo de 2006
[EDM] Entity Data Model. ADO.NET Technical Preview, mayo de 2006.
[ESQL] eSQL: An Entity SQL Language, ADO.NET Technical Preview, mayo de 2006.
[ARCH] ADO.NET Entity Framework architecture. ADO.NET Technical Preview, mayo de 2006.
[LINQ] The LINQ Project - .NET Language Integrated Query, septiembre de 2005
[DLINQ] Presentación de DLinq para desarrolladores de C# y Presentación de DLinq para desarrolladores de Visual Basic, mayo de 2006
Apéndice
6.1 Esquema EDM de ejemplo representado como XML
<?xml version="1.0" encoding="utf-8" ?>
<Schema Namespace="AdventureWorks" xmlns="urn:schemas-
Microsoft-com:windows:storage" >
<EntityContainerType Name="AdventureWorksDB">
<Property Name="SalesOrders" Type="EntitySet(SalesOrder)" />
<Property Name="SalesPeople"
Type="EntitySet(SalesPerson)" />
<Property Name="SalesPersonOrders"
Type="Relacioneset(SalesPerson_Order)">
<End Name="SalesPerson" Extent="SalesPeople"/>
<End Name="Order" Extent="SalesOrders" />
</Property>
</EntityContainerType>
<!—Jerarquía de tipos de Sales Order (Pedido)-->
<EntityType Name="SalesOrder" Key="ID">
<Property Name="ID" Type ="System.Int32"
Nullable="false"/>
<Property Name="OrderDate" Type ="System.DateTime"
Nullable="true"/>
<Property Name="Status" Type ="System.Byte"
Nullable="true"/>
<Property Name="AccountNumber" Type ="System.String"
Nullable="true"
Size="15"/>
<Property Name="TotalDue" Type ="System.Decimal"
Nullable="true"/>
</EntityType>
<EntityType Name="StoreSalesOrder" BaseType="SalesOrder" >
<Property Name="Tax" Type ="System.Decimal" Nullable="true"/>
</EntityType>
<!—Tipo de entidad Person (Persona)-->
<EntityType Name="SalesPerson" Key="ID">
<!—Propiedades de SalesPerson (Vendedor)-->
<Property Name="ID" Type="System.Int32" Nullable="false"/>
<Property Name="SalesQuota" Type="System.Decimal"/>
<Property Name="Bonus" Type="System.Decimal"/>
<Property Name="SalesYTD" Type="System.Decimal"/>
<!—Propiedades de la tabla Employee (Empleados)-->
<Property Name="HireDate" Type="System.DateTime" Nullable="true"/>
<Property Name="Title" Type="System.String"
Nullable="true" Size="50"/>
<!—Propiedades de la tabla de contactos-->
<Property Name="FirstName" Type="System.String"
Nullable ="true" Size="50"/>
<Property Name="MiddleName" Type="System.String"
Nullable ="true"
Size="50"/>
<Property Name="LastName" Type="System.String"
Nullable ="true" Size="50"/>
<Property Name="ContactInformation"
Type="AdventureWorks.ContactInfo"
Nullable="false"/>
</EntityType>
<ComplexType Name="ContactInfo">
<Property Name="EmailAddress" Type="System.String"
Nullable="true"
Size="50"/>
<Property Name="Phone" Type="System.String" Nullable
="true" Size="25"/>
</ComplexType>
<Association Name="SalesPerson_Order">
<End Name="Order" Type="SalesOrder" Multiplicity="*"
PluralName="Orders"/>
<End Name="SalesPerson" Type="SalesPerson"
Multiplicity="1" />
</Association>
</Schema>
6.2 Mapeado de ejemplo representado como XML
<?xml version="1.0" encoding="utf-8" ?>
<Mapping xmlns="urn:schemas-Microsoft-com:windows:storage:mapping:CS"
xmlns:cdm="urn:schemas-Microsoft-com:windows:storage:mapping:CS"
cdm:Space="C-S">
<EntityContainerMapping cdm:CdmEntityContainer="AdventureWorks.AdventureWorksDB"
cdm:StorageEntityContainer="AdventureWorksTarget.dbo">
<EntitySetMapping cdm:Name="SalesOrders">
<EntityTypeMapping cdm:TypeName="AdventureWorks.StoreSalesOrder">
<TableMappingFragment cdm:TableName="SalesOrder">
<EntityKey>
<ScalarProperty cdm:Name="ID" cdm:ColumnName="SalesOrderID" />
</EntityKey>
<ScalarProperty cdm:Name="OrderDate" cdm:ColumnName="OrderDate" />
<ScalarProperty cdm:Name="Status" cdm:ColumnName="Status" />
<ScalarProperty cdm:Name="AccountNumber"
cdm:ColumnName="AccountNumber" />
<ScalarProperty cdm:Name="TotalDue" cdm:ColumnName="TotalDue" />
<ScalarProperty cdm:Name="Tax" cdm:ColumnName="TaxAmt" />
<Condition cdm:ColumnName="OnlineOrderFlag" cdm:Value="false" />
</TableMappingFragment>
</EntityTypeMapping>
<EntityTypeMapping cdm:TypeName="AdventureWorks.SalesOrder">
<TableMappingFragment cdm:TableName="SalesOrder">
<EntityKey>
<ScalarProperty cdm:Name="ID" cdm:ColumnName="SalesOrderID" />
</EntityKey>
<ScalarProperty cdm:Name="OrderDate" cdm:ColumnName="OrderDate" />
<ScalarProperty cdm:Name="Status" cdm:ColumnName="Status" />
<ScalarProperty cdm:Name="AccountNumber"
cdm:ColumnName="AccountNumber" />
<ScalarProperty cdm:Name="TotalDue" cdm:ColumnName="TotalDue" />
<Condition cdm:ColumnName="OnlineOrderFlag" cdm:Value="true" />
</TableMappingFragment>
</EntityTypeMapping>
</EntitySetMapping>
<EntitySetMapping cdm:Name="SalesPeople">
<EntityTypeMapping cdm:TypeName="AdventureWorks.SalesPerson">
<TableMappingFragment cdm:TableName="SalesPerson">
<EntityKey>
<ScalarProperty cdm:Name="ID"
cdm:ColumnName="SalesPersonID" />
</EntityKey>
<ScalarProperty cdm:Name="SalesQuota"
cdm:ColumnName="SalesQuota" />
<ScalarProperty cdm:Name="Bonus"
cdm:ColumnName="Bonus" />
<ScalarProperty cdm:Name="SalesYTD" cdm:ColumnName="SalesYTD" />
</TableMappingFragment>
<TableMappingFragment cdm:TableName="Employee">
<EntityKey>
<ScalarProperty cdm:Name="ID" cdm:ColumnName="EmployeeID" />
</EntityKey>
<ScalarProperty cdm:Name="HireDate" cdm:ColumnName="HireDate" />
<ScalarProperty cdm:Name="Title" cdm:ColumnName="Title" />
</TableMappingFragment>
<TableMappingFragment cdm:TableName="Contact">
<EntityKey>
<ScalarProperty cdm:Name="ID" cdm:ColumnName="ContactID" />
</EntityKey>
<ScalarProperty cdm:Name="FirstName" cdm:ColumnName="FirstName" />
<ScalarProperty cdm:Name="MiddleName" cdm:ColumnName="MiddleName" />
<ScalarProperty cdm:Name="LastName" cdm:ColumnName="LastName" />
<ComplexProperty cdm:Name="ContactInformation"
cdm:TypeName="AdventureWorks.ContactInfo">
<ScalarProperty cdm:Name="EmailAddress"
cdm:ColumnName="EMailAddress"/>
<ScalarProperty cdm:Name="Phone" cdm:ColumnName="Phone"/>
</ComplexProperty>
</TableMappingFragment>
</EntityTypeMapping>
</EntitySetMapping>
<AssociationSetMapping cdm:Name="SalesPersonOrders"
cdm:TypeName="AdventureWorks.SalesPerson_Order"
cdm:TableName="SalesOrder">
<EndProperty cdm:Name="SalesPerson">
<ScalarProperty cdm:Name="ID" cdm:ColumnName="SalesPersonID"/>
</EndProperty>
<EndProperty cdm:Name="Order">
<ScalarProperty cdm:Name="ID" cdm:ColumnName="SalesOrderID" />
</EndProperty>
</AssociationSetMapping>
</EntityContainerMapping>
</Mapping>
