el Guille, la Web del Visual Basic, C#, .NET y más...

Novedades de Visual Basic 9.0

(Parte 5 de 9)

 
Publicado el 22/Jul/2007
Actualizado el 23/Jul/2007
Autor: Guillermo 'guille' Som

Novedades de Visual Basic 9.0 (Parte 5 de 9): Expresiones de consultas.



 

Expresiones de consultas

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.

 

Usar LINQ con una base de datos

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
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)


Código de ejemplo (comprimido):

Para bajarte el código, hazlo desde la página principal de
Las Novedades de Visual Basic 9.0 con la CTP de Junio 2007.


 


La fecha/hora en el servidor es: 22/12/2024 6:11:43

La fecha actual GMT (UTC) es: 

©Guillermo 'guille' Som, 1996-2024