Tech·Ed by raona

Wednesday, November 29, 2006

DEV306 - LINQ to SQL

Buenooo... una nueva sesión sobre LINQ, continuación de la anterior DEV323 (C# 3.0). En este caso se trataba de explorar las capacidades de LINQ para el mapeo de entidades relacionales de la BBDD a objetos de negocio.

Antes que nada un repaso a los principales problemas de usar comandos SQL embebidos en cadenas para acceder a la BBDD:
  1. Queries son cadenas, por lo tanto el compilador no puede ayudar.
  2. Parámetros son loosely-bound y tipo de retorno es un loosely-typed resultset, por lo tanto no se pueden hacer chequeos en tiempo de compilación.
La idea de LINQ to SQL es la misma que la multitud de ORM mappers que existen hoy día: generar un conjunto de clases para representar la estructura de datos de la BBDD subyacente. En el caso de LINQ:
  1. La conexión a la BBDD és una clase derivada de DataContext
  2. Tablas y vistas son convertidas a clases
  3. Columnas y Relaciones son propiedades de las clases.
  4. Stored procedures son métodos.
Generación del mapping

Para crear el mapping entre la BBDD y las clases de LINQ:
  1. Decidir que tablas de la BBDD se quieren mapear a clases .NET
  2. Usar el atributo [Column] a las propiedades que se quieran mapear.
    1. Usar [Column(Id=true)] para la clave primaria.
  3. Usar el atributo [Table(Name="Foo")] para mapear tablas.
  4. Declarar la clase Db derivada de DataContext.
    1. Añadir miembros Table<T> siendo T las clases Table que hemos creado.
    2. Invocar al constructor de DataContext con la cadena de conexión correspondiente.
  5. Crear métodos en la clase Db para los stored procedures y usar el atributo [StoredProcedure] para mapearlos.
  6. Crear objeto Db y lanzar consultas usando query expressions.
    1. LINQ convierte la query expression en consultas SQL de forma automática.
Otra opción es usar un fichero XML que tenga información sobre el mapping, en lugar de tener que usar atributos: las clases deben crearse igualmente pero sin usar ningún atributo, y al crear la clase Db se crea pasando un parámetro XmlMappingSource(XmlMappingSource.FromUrl("..."));

La tercera opción para generar el mapeo es usar Visual Studio: Project -> Add -> LINQ to SQL Mappint y usar el server explorer de Visual Studio, para arrastrar tablas y generar el código (genera partial clases).

Así el mapping es flexible:
  • Podemos crear las clases primero y luego decidir cuales de ellas y como las mapeamos a la BBDD.
  • Podemos crear la BBDD primero y luego generar todas las clases de mapping.

Consultas de datos

Para consultar objetos se usan las query expressions.
P.ej. dada la siguiente clase:

public class Northwind : DataContext
{
public Table<Customer> Customers;
}

donde Customer es una clase con el atributo [Table] aplicado, entonces una vez creado un objeto Norhwind (llamésmole db) entonces para realizar consultas:

// Escojer los customers de NY amb 5 o mes ordres
from c in db.Customers
where c.City == "NY " and c.Orders.Count >= 5
select new {c.CompanyName, c.OrderID};

// Escoger los customers de NY con sus Orders del 1997
from c in db.Customers
where c.City == "NY"
from o in c.Orders
where o.OrderDate.Value.Year = 1997
select new {c.CompanyName, c.OrderID, o.OrderDate};

LINQ asegura que existe un sólo objeto en memoria para una misma PK de una tabla (identity mapping). Es decir, si mediante una query obtenemos un registro con una determinada PK, y luego mediante otra query obtenemos otra vez el mismo registro, el objeto devuelto por LINQ será el mismo.

var cust = db.Customers.Single (c => c.CustomerID == "CONSH");
var query = from c in db.Customers select c;
foreach (var c in query) { Console.WriteLine( c==cust);}


En la primera línea se guarda en la variable cust, el objeto Customer que representa al registro con la PK 'CONSH'.
En la segunda línea se obtiene un IEnumerable de Customers con todos los customers.
En la tercera línea se itera sobre el IEnumerable obtenido y por cada objeto Customer contenido en la lista se imprime si es el mismo que el objeto referenciado por cust.
Este código imprime tantos "false" como registros haya en la tabla de la BBDD, excepto una vez que imprime "true" (correspondiente al Customer 'CONSH').
Es decir, la variable cust y la colección query contienen referencias al mismo objeto Customer, por tanto una modificación realizada desde cualquier sitio será visible al resto (cosa lógica, puesto que es el mismo registro de la BBDD).

La navegación entre las propiedades equivale a las joins clásicas de BBDD:

var query =
from o in db.Orders
where o.OrderDetails.Any (d => d.Product.ProductName == "Tofu")
select new {o.Customer.Company.CompanyName, o.OrderID}

Escoje todas las Orders que tengan alguna OrderDetail que contenga un producto llamado "Tofu". Es decir equivale a una join entre Order, OrderDetail y Product.

En las query expressions se usa la palabra clave join, cuando se quieren unir resultados que no tienen relación de FK en la BBDD (si hay relación de FK en la BBDD entonces se usan las propiedades como en el ejemplo anterior).

from c in db.Customers
join s in db.Suppliers on c.Country equals s.Country
select new
{
CustomerName = c.CompanyName,
SupplierName = s.CompanyName
};

Devuelve el nombre de un Customer y de sus Suppliers que sean del mismo país. Es decir realiza una join entre el pais de un Consumer y de un Supplier. Esa relación no existe en la BBDD en forma de FK.

En LINQ cuando se declara una query:

var query = from c...

La query se prepara pero no se ejecuta hasta que se necesario acceder a los resultados. Eso hace posible usar una query dentro de otra query y que sólo se envie a la BBDD el SQL combinado más óptimo:

var query1 = from c in db.Customers where c.City == "NY" select c;
var query2 = from c in query1 select new { c.CompanyName;}


La primera query selecciona todos aquellos Customers de NY.
La segunda query selecciona el nombre de la empresa de todos los customers devueltos por la primera query.
Cuando se acceden a los resultados devueltos por query2 es cuando esta se ejecuta y lo que se hace es enviar a la BBDD el SQL combinado de las dos queries. Es decir el SQL que se enviaría a la BBDD sería algo como:

SELECT companyName FROM CUSTOMERS WHERE CITY = 'NY'

Modificación de datos

Para insertar datos se llama al método Add de la clase tabla correspondiente y luego el método SubmitChanges.

db.Customers.Add(myNewCustomer);
db.SubmitChanges();

Para modificar datos, se obtiene el objeto que deseamos modificar, modificamos sus propiedades y luego llamamos a SubmitChanges:

// Escogemos un customer
Customer cust = db.Customers.Single ( c => c.CustomerID == "EXISTINGID");
cust.ContactName = "A new Name";
db.SubmitChanges();

Los updates cuando se traducen a SQL sólo modifican las propiedades modificadas (no todas).
Para tratar los conflictos de concurrencia de modificaciones LINQ aplica una aproximación optimistica: Al realizar el UPDATE añade una cláusula WHERE con todos las propiedades tal y como estaban cuando se recuperó el registro. De esta manera si alguien ha modificado el registro antes el UPDATE no tiene efecto. Si tenemos en la BBDD alguna columna tipo rowid se puede indicar a LINQ que haga el WHERE contra esa columna en lugar de contra todas las propiedades.

Acceso a SQL

Bueno... todo eso de LINQ está muy bien, pero a veces nos puede interesar realizar consultas directamente usando SQL. Para ello LINQ proporciona el método ExecuteQuery en la clase DataContext. Es un método genérico, y el parámetro genérico sirve para indicar el tipo devuelto por la query, para de esa manera poder tener resultsets tipados:

var query = db.ExecuteQuery<Contact>("Select CompanyName as Name, Phone from Customers where city = 'NY'");

Bueno.... básicamente eso fue todo lo que dio de si esa charla... bastante información y ejemplos constantes. En mi opinión fue una muy buena charla (divertida, lo que se dice divertida, no... pero interesante sí).

0 Comments:

Post a Comment

<< Home