En inglés: Query expressions. tanto en Visual Basic 9.0 como en
C# 3.0 (los que vienen en la versión 2008 de Visual Studio, sí, ya sé que lo
sabes, pero...) se han añadido instrucciones al lenguaje para manejar las
consultas al estilo de SQL. Y tal como viste en el código de los listados 1
y 2, se pueden usar para "extraer" datos de "algo" que sea enumerable, es
decir, de cualquier colección o de cualquier array.
Ese "algo" puede ser, por ejemplo, el resultado de una consulta a una
base de datos, aunque también puede ser cualquier array o colección.
Cualquier cosa que se pueda usar con For Each, se puede
"consultar" con las expresiones de consultas.
La forma de crear expresiones de consultas (o expresiones LINQ) es:
From <variable> In <colección> [cláusulas]
[Select colExp1 [, colExp2]]
Ahora te explico "más o menos" cada parte.
En Visual Basic 9.0 se pueden usar estas "instrucciones" (cláusulas) en
las consultas de LINQ:
- Aggregate
- Distinct
- From
- Group By
- Group Join
- Join
- Order By
- Select
- Skip
- Skip While
- Take
- Take While
- Where
Del significado de cada instrucción, pues... será en otra ocasión, ahora
solamente te voy a contar un poco de esto, aunque si conoces algo de las
consultas SQL, pues... te puedes hacer una idea.
También hace su "reaparición" una instrucción que es de los viejos
tiempos de BASIC (así, en mayúsculas), que es la
instrucción Let. En este caso, el uso es el mismo que tenía
"originalmente" y es la de asignar un valor a una variable, en esta ocasión
su "reaparición" (¿o sería más correcto decir reencarnación?) es para que el
compilador sepa cuándo queremos asignar un valor dentro de una expresión de
consulta (ahora te muestro un ejemplo).
Para crear una expresión de consulta, debes tener en cuenta que lo que se
devuelve es un objeto del tipo IEnumerable(Of T) dónde
T será del tipo que se indique después de Select.
Aunque en Visual Basic, Select es totalmente opcional, de
forma que si no lo indicas, el propio compilador "adivinará" qué es lo que
tiene que devolver.
Para crear una expresión LINQ, debes usar la instrucción From
seguida de una variable que será la que se usará para "recorrer" los
elementos a enumerar. El compilador "inferirá" el tipo según lo que
indiquemos después de la instrucción In. Puedes poner
varios "grupos" de datos a utilizar con From, simplemente
separándolos con comas, esto sería equivalente a usar un INNER JOIN de SQL.
Después indicarás las "condiciones" que se tendrán en cuenta, aquí es donde
entran a juego las "cláusulas" que te he mostrado en la lista anterior.
Por último, para saber qué datos son los que se devuelven, tendrás que usar
Select (que como te he dicho, es opcional) seguida de la "columna" (o
columnas) que quieres devolver. El propio compilador "creará" un tipo
adecuado a esos datos que quieras devolver (en realidad un tipo anónimo).
Para que te hagas una idea, mira esto:
Dim nombres = New String() {"Pepe", "Juan", _
"Eva", "Lourdes", _
"Pedro", "Carmen", _
"Luis", "Leticia"}
Dim q1 = From n In nombres _
Let pos = 2 _
Where n.Length > 2 AndAlso "aeiou".IndexOf(n(pos)) > -1 _
Order By n Descending _
Let Desc = "Nombre con una vocal en la posición " & pos & ": " & n _
Select Desc, n
For Each n1 In q1
Console.WriteLine(n1.Desc, n1.n)
Next
Listado 3. Ejemplo de LINQ
En el código del listado 3 se está "analizando" el contenido del array
nombres, la variable "n" irá tomando cada uno de esos nombres.
La variable
pos es "interna" a la consulta, por tanto, la definimos con
Let.
Comprobamos que solo se tengan en cuenta las que cumplan la condición de la
cláusula Where, en este caso, que la longitud sea mayor de 2 y que en la
posición indicada por la variable pos tenga una vocal.
Si se cumple esa
condición se ordenará de forma descendente por el valor de la variable
n,
(en este caso, el nombre completo, pero también podrías usar algo como
n.Substring(1, 2)).
Además se crea otra variable "interna" a la consulta a la que llamamos
Desc
y le asignamos esa cadena. Finalmente se "seleccionarán" como valores a
devolver lo que tenga esa variable además de la variable n.
En este caso, si omitimos la cláusula Select, el valor que se devuelve es
el mismo, es decir, un tipo anónimo con el contenido de n y de
Desc.
Vale, mu bonito. ¿Eso es todo?
Pues no, porque si haces algo como esto:
nombres(0) = "Eduardo"
Console.WriteLine("Después de cambiar un dato")
For Each n1 In q1
Console.WriteLine(n1.Desc, n1.n)
Next
Es decir, cambias uno de los elementos de la lista de nombres, y resulta
que ese elemento coincide con lo que hay en el Where, pues también se
mostrará. Lo siento Pepe, pero la consulta si que prefiere a
Eduardo.
Lo que no puedes hacer es añadir más elementos, pero si cambias alguno de
los que hay, al volver a "ejecutar" la consulta, se volverá a procesar.
Por supuesto, también puedes usar una base de datos:
Dim db As New NorthwindDataContext
Dim productos = From prod In db.Products _
Where prod.CategoryID > 4 _
Order By prod.CategoryID _
Select prod
For Each p In productos
Console.WriteLine("{0}, {1}", p.CategoryID, p.ProductName)
Next
Y si esos datos cambian, se actualizará la consulta. En este último
ejemplo, la consulta está en la variable productos.
Y si quieres un ejemplo de código SQL y una "posibilidad" con LINQ,
pues...
El código de SQL:
SELECT DISTINCTROW
CompanyName FROM Customers
INNER JOIN Orders
ON Customers.CustomerID = Orders.CustomerID
ORDER BY CompanyName
El código de LINQ (VB9):
Dim db As New NorthwindLINQToSQLDataContext
Dim q1 = From cli In db.Customers _
Join ord In db.Orders _
On cli.CustomerID Equals ord.CustomerID _
Order By cli.CompanyName _
Select cli.CompanyName Distinct
Fíjate en el detalle de que hay que usar Equals en lugar
de el signo de igualdad.
Hay muchas más cosas que se pueden decir de las "expresiones de consulta"
(o consultas LINQ), pero... vamos dejarlo así y veamos más cosas nuevas de
Visual Basic 9.0.
Algo en lo que también debes fijarte es que para acceder a los datos,
estoy usando un "objeto" del tipo LINQ To SQL, (también
conocido como Object Relational Designer -O/R
Designer-), que es una forma muy sencilla de añadir "clases"
basadas en las tablas de una base de datos, además el diseñador de Visual
Studio 2008 te las muestra como lo hace el diseñador de clases (ver la
figura 3).
Figura 3. Diseñador de clases LINQ To SQL
Por supuesto, puedes añadir las tablas que vayas a usar e incluso solo
los campos que elijas de cada tabla.
Lo que no puedes hacer es usar dos conexiones distintas. Solo se permite
una conexión en cada "objeto" LINQToSQL.
Hay muchas más cosas que se puede hacer con lo que se conoce como LINQ To
SQL (antes conocido como DLINQ), como es la definir una clase que se
"mapeará" con una tabla de la base de datos...
Valeee... un ejemplo.
Defines una clase "mapeada" a una tabla, por ejemplo:
<Table(Name:="Customers")> _
Public Class Customer
Public ContactName As String
Public Country As String
End Class
Después, simplemente te conectas a la base de datos, pero usando un
objeto del tipo DataContext, que puede hacer referencia a una cadena de
conexión o a un fichero .mdf (en este último caso, solo tendrías que indicar
el path completo, al estilo de como se hace con una base de Access).
De ese objeto DataContext, le dices que obtenga la tabla a la que
"mapea" el tipo de datos que acabamos de definir:
Dim sCnn = "Data Source = (local)\SQLEXPRESS; " & _
"Initial Catalog = Northwind; " & _
"Integrated Security = True"
Dim dc As New DataContext(sCnn)
Dim losClientes = dc.GetTable(Of Customer)()
Y... el resto, pues como si ya tuvieras la tabla (con solo esos campos
que has definido en el tipo que "mapea" a la tabla),
por tanto, podemos hacer algo como esto:
Dim pais = "s"
Dim q1 = From c In losClientes _
Where c.Country.Contains(pais) _
Order By c.ContactName _
Select c
For Each c In q1
Console.WriteLine("{0}, {1}", c.ContactName, c.Country)
Next
El nombre de la clase puede ser el que quieras, no tiene porqué llamarse como
la tabla. El atributo es en realidad el que se encarga de saber qué tabla debe
mapear.
Si cambias el nombre de la clase, por ejemplo a: MiCliente, en
el método GetTable del objeto DataContext
tendrás que usar el nombre que le hayas dado a la clase.
Por ejemplo, si quieres acceder a los datos de la tabla Employees, puedes
crear una clase que se llame MiEmpleado, pero que esté "mapeada" a la tabla
Employees de Northwind, y, por supuesto, que los campos o
propiedades que
definan deben coincidir con los nombres de los campos de la tabla.
<Table(Name:="Employees")> _
Public Class MiEmpleado
Public LastName As String
Public FirstName As String
Public BirthDate As Date
End Class
Ahora para obtener esta tabla, tendremos que indicar en GetTable
la clase que hemos definido:
Dim sCnn = "Data Source = (local)\SQLEXPRESS; " & _
"Initial Catalog = Northwind; " & _
"Integrated Security = True"
Dim dc As New DataContext(sCnn)
Dim losEmpleados = dc.GetTable(Of MiEmpleado)()
Y ya podremos usar "la tabla mapeada" en la variable losEmpleados
para realizar una consulta.
En este caso, para que veas más posibilidades de los tipos anónimos,
vamos a crear un tipo para mostrar los datos de los empleados que cumplan el
criterio que le vamos a indicar, en este caso, que el mes de la fecha de
nacimiento sea superior a 5.
Debido a que en la clase hemos definido solo tres campos, esos serán los
que podremos usar en la consulta y, por supuesto en los resultados. Pero en
lugar de usar el nombre y el apellido por separado, vamos a crear un tipo
anónimo que "una" esos dos datos y además vamos a crear una propiedad para
la fecha, pero en lugar de llamarla BirthDate, se llamará
Fecha y la usaremos con un formato más "práctico"... por
decir algo, je, je.
' Podemos crear un nuevo tipo anónimo como resultado de la consulta
Dim q1 = From e In losEmpleados _
Order By e.LastName _
Where e.BirthDate.Month > 5 _
Select New With { _
.Nombre = e.LastName & ", " & e.FirstName, _
.Fecha = "Cumpleaños: " & e.BirthDate}
For Each d In q1
Console.WriteLine("{0}, {1}", d.Nombre, d.Fecha)
Next
Aunque no todo lo práctico que me hubiera gustado, ya que no se puede
usar "ToString" con formatos en la fecha... Por ejemplo,
para que devolviera algo como esto: e.BirthDate.ToString("s"),
y la razón es porque aunque en la clase definamos un tipo Date (o DateTime)
en realidad, el tipo de datos de la tabla es Nullable(Of DateTime) o
Date?,
y ese tipo no tiene sobrecargas para el método ToString.
En cualquier caso, si en lugar de "crear" una propiedad especial simplemente
la asignas con el mismo tipo, después en el bucle podrás usar ToString:
Dim q2 = From e In losEmpleados _
Order By e.LastName _
Where e.BirthDate.Month > 5 _
Select New With { _
.Nombre = e.LastName & ", " & e.FirstName, _
.Fecha = e.BirthDate}
For Each d In q2
Console.WriteLine("{0}, {1}", d.Nombre, d.Fecha.ToString("dd/MMM/yyyy"))
Next
Nota:
Para que esto funcione, hay que añadir una referencia a
System.Data.Linq.dll y añadir las importaciones para
System.Data.Linq y System.Data.Linq.Mapping, la
primera para los tipos (DataContext) y la segunda para el
atributo para "mapear" la tabla.
Y se pueden hacer muchas más cosas... Pero... tendrá que ser en otra ocasión...
Aunque en el código de ejemplo tienes también cómo usar vistas y
procedimientos almacenados por medio del Object Relational Designer
para que veas lo fácil que es esto... al menos menos "rollo" que los
DataSet tipados. En el código te explico los pasos que tienes que
dar para crear el objeto con el diseñador relacional (O/R designer).
Ir al índice de las
novedades de Visual Basic 2008 (VB 9.0)