Welcome to Comunidad .NET de Cd. Juárez Sign in | Join | Help

Browse by Tags

All Tags » ado.net

Cómo leer archivos planos con ADO.NET (versión Visual Basic 2005)

Hace poco más de un año escribí este artículo que describe una técnica para leer archivos planos utilizando el OleDB provider de ADO.NET.  Es uno de los artículos de este sitio que ha recibido más comentarios, y entre ellos está uno que dejó fredy que me hizo re-hacer el ejemplo en Visual Basic 2005 para comprobar que no fuera un error de código—en realidad él hizo la mayor parte de la chamba para "traducir" la rutina.

No voy a explicar mucho la lógica del código—para eso te dejo de tarea que leas el artículo original—aquí simplemente te comparto cómo se vería la rutina en VB:

Imports System.IO
Imports System.Data
Imports System.Data.OleDb
 
Module Utilerias
   Public Enum TipoDeArchivoPlano
        Delimited
        Fixed
   End Enum
 
   Public Function LeerArchivoPlano(ByVal archivo As FileInfo, _
        ByVal tieneEncabezado As Boolean, _
        ByVal tipoDeArchivo As TipoDeArchivoPlano) As DataTable
 
        If (Not archivo.Exists) Then
            Throw New FileNotFoundException("No se encontró el archivo especificado")
        End If
 
        Dim conEncabezado As String = IIf(tieneEncabezado, "YES", "NO")
 
        Dim connectionString As String = _
            String.Format("Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};" + _
            "Extended Properties='text;HDR={1};FMT={2}'", _
            archivo.DirectoryName, conEncabezado, tipoDeArchivo.ToString())
 
        Dim dt As DataTable = New DataTable("miTabla")
 
        Using conn As OleDbConnection = New OleDbConnection(connectionString)
            Using da As OleDbDataAdapter = New OleDbDataAdapter( _
               "SELECT * FROM " + archivo.Name, conn)
 
               da.Fill(dt)
            End Using
        End Using
 
        Return dt
   End Function
End Module

Para probarla, hice una aplicación sencilla en ASP.NET que mostrara los datos de un archivo .CSV que está dentro de un subdirectorio del sitio web.

El archivo jason.csv contiene:

Producto,Cantidad,Precio
Sierra eléctrica,1,250
Máscara de hockey,1,15.50
Machete,5,2.70
Detergente para ropa (con quita-manchas),1,10
Delantal,2,7.25
Afilador,3,5

La página dentro de la solución que en realidad solo tiene un GridView.  Este es el contenido de Default.aspx:

<%@ Page Language="VB" AutoEventWireup="false" 
   CodeFile="Default.aspx.vb" Inherits="_Default" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
   <title>Leer un archivo plano con VB 2005</title>
</head>
<body>
   <form id="miFormulario" runat="server">
   <div>
        <asp:GridView ID="miGridView" runat="server">
        </asp:GridView>
   </div>
   </form>
</body>
</html>

Finalmente, para mandar llamar la rutina y bindear—¿enlazar?—los datos al GridView, solo agregué esto en el code-behind:

Partial Class _Default
   Inherits System.Web.UI.Page
 
   Protected Sub miFormulario_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
        Handles miFormulario.Load
 
        If Not Page.IsPostBack Then
            Dim archivo As FileInfo = _
               New FileInfo("D:\WebSites\LeerArchivosPlanosVB\Archivos\jason.csv")
 
            Dim tabla As New DataTable
            tabla = Utilerias.LeerArchivoPlano(archivo, True, TipoDeArchivoPlano.Delimited)
 
            If tabla.Rows.Count > 0 Then
               miGridView.DataSource = tabla
               miGridView.DataBind()
            Else
               Response.Write("No hay datos para mostrar.")
            End If
 
        End If
   End Sub
End Class

El resultado de correr la página:

 

Whew! Funcionó smile_teeth

Enjoy. smile_shades


PD.  Puedes descargar el código de este ejemplo del sitio de la Comunidad .NET de Cd. Juárez.

Posted from Diario de un Dotnetero | 0 Comments
Filed under: , , ,

Cómo leer archivos planos con ADO.NET

(Antes que me la rieguen, sí, ya sé que son "archivos de texto sencillo" pero este blog está en pocho, ¿no? Digo, no hay archivos de computadora planos, o cúbicos o esféricos, que yo sepa, pero así les dicen muchos desarrolladores)

Aunque hoy en día ya todo mundo debería de estar utilizando (según yo) XML para el intercambio de información, sigue siendo muy comun que para mandar información de un sistema a otro se haga a través de archivos "planos" de texto.

Si alguna vez has lideado con sistemas legacy o en plataformas chiples (mainframe, or SAP anyone?) estoy seguro que sabes de lo que hablo. Otro caso común es cuando requieres que tu usuario genere algún archivo para cargar esos datos en tu aplicación. Harta cantidad de gerentoides y chalanes no saben ni (bleep) de sistemas o de computadoras, pero eso sí, son masters sensai del Excel, así que es fácil decirles que le den un "Save As... CSV" a un archivo para subir sus datos.

Ahora, ¿cómo le harías para leer esos archivos con .NET? Si eres entusiasta probablemente luego luego te las ingeniarías para usar un FileStream y parsear el contenido línea por línea, dar 3 maromas... qué se yo. Sin embargo, existe un truco sencillo que puede ahorrarte broncas: utilizar el OLEDB Provider de ADO.NET para hacerlo. Esta técnica funciona bastante bien para leer archivos CSV o archivos de texto con columnas en posiciones fijas (si son secuenciales, ya valiste cake).

Leyendo un archivo CSV

Por ejemplo, imagina que eres achichincle del Jason (el de las películas de terror) y te manda a hacer sus compras, de lo contrario terminarás con tu cabeza y cuerpo en diferentes sectores de la ciudad. El vato hace su "chopin list" en Excel y lo guarda en el siguiente archivo llamado jason.csv:

Producto,Cantidad,Precio
Sierra eléctrica,1,250
Máscara de hockey,1,15.50
Machete,5,2.70
Detergente para ropa (con quita-manchas),1,10
Delantal,2,7.25
Afilador,3,5

Entonces, como buen dotnetero, podrías leerlo con una rutina como esta:

// asumiendo que tenemos
// using System.Data.OleDb;
// using System.Data;
 
// en este connection string:
//     HDR=Yes       : indica que el primer registro contiene los encabezados 
//                     (nombres) de las columnas, no datos.
//     FMT=Delimited : indica que el los campos están delimitados por un caracter
//                     (coma por default).
string connectionString =
    @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\DirectorioDeArchivosCSV;" +
    "Extended Properties='text;HDR=Yes;FMT=Delimited'";
 
DataTable dt = new DataTable("miTabla");
using (OleDbConnection conn = new OleDbConnection(connectionString))
using (OleDbDataAdapter da = new OleDbDataAdapter("SELECT * FROM jason.csv", conn))
{
    da.Fill(dt);
}
 
// hacer algo con los datos en el DataTable

El código es bastante sencillo, es el patrón estándar para usar un DataAdapter. Lo único de especial que tiene es que utiliza el OLEDB Data Provider, y que en el connection string le especificamos el directorio donde se encuentra el archivo, así como el formato que tiene. Nota que emites un SELECT de SQL común y corriente, por lo cual podrías agregar una cláusula WHERE si así lo quisieras.

En fin, para comprobar que en realidad funcionara el código, puse un breakpoint e invoqué el DataSet Visualizer desde Visual Studio. El resultado:

Pero, ¿qué tan inteligente es el OLEDB Provider? ¿Adivinó correctamente el tipo de mis datos?


Leyendo un archivo de texto con posiciones fijas

Ahora, asume que el méndigo Jason te la puso más difícil y en lugar de darte un archivo CSV, te da un archivo de texto sencillo como este (jason.txt):


El código sería muy similar al anterior:

// asumiendo que tenemos
// using System.Data.OleDb;
// using System.Data;
 
// en este connection string:
//     HDR=Yes   : indica que el primer registro contiene los encabezados 
//                 (nombres) de las columnas, no datos.
//     FMT=Fixed : indica que el los campos están en posiciones fijas y el tamaño
//                 de cada campo se especifican con un archivo SCHEMA.INI
//                 en el mismo directorio donde está el archivo a leer.
string connectionString =
    @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\DirectorioDeArchivosTXT;" +
    "Extended Properties='text;HDR=Yes;FMT=Fixed'";
 
DataTable dt = new DataTable("miTabla");
using (OleDbConnection conn = new OleDbConnection(connectionString))
using (OleDbDataAdapter da = new OleDbDataAdapter("SELECT * FROM jason.txt", conn))
{
    da.Fill(dt);
}
 
// hacer algo con los datos en el DataTable

De hecho, lo único que cambió fue el parámetro FMT en el connectionString, el nombre del archivo y el directorio donde localizarlo. Sin embargo, para que esto funcione con archivos de posiciones fijas es necesario un paso adicional: especificar el tamaño (y tipo) de las columnas en el archivo.

Esto se hace mediante un archivo schema.ini que debe estar en el mismo directorio que el archivo que vas a leer. Consulta esta página para saber todas las opciones disponibles. En nuestro caso un archivo como el siguiente sería suficiente:

[jason.txt]
Format=FixedLength
Col1=Producto Char Width 40
Col2=Cantidad Long Width 10
Col3=Precio Double Width 10

Factorizando código

Si generalizamos el código un poco, podemos extraer una función sencilla que pueda ser reutilizada en varios de nuestros programas. Esa rutina podría ser como esta, en donde le pasas como parámetros el tipo y archivo a leer y te regresa un DataTable poblado ya con los datos:

// asumiendo que tenemos 
// using System.Data.OleDb;
// using System.Data;
// using System.IO;
 
public enum TipoDeArchivoPlano { Delimited, Fixed }
 
public static DataTable LeerArchivoPlano(
    FileInfo archivo, bool tieneEncabezado, TipoDeArchivoPlano tipoDeArchivo )
{
    if (!archivo.Exists)
        throw new FileNotFoundException(
            "No se encontró el archivo especificado");
 
    string conEncabezado = tieneEncabezado ? "YES" : "NO";
 
    string connectionString = String.Format(
        @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};" +
        "Extended Properties='text;HDR={1};FMT={2}'",
        archivo.DirectoryName, conEncabezado, tipoDeArchivo.ToString());
 
    DataTable dt = new DataTable("miTabla");
    using (OleDbConnection conn = new OleDbConnection(connectionString))
    using (OleDbDataAdapter da =
        new OleDbDataAdapter("SELECT * FROM " + archivo.Name, conn))
    {
        da.Fill(dt);
    }
 
    return dt;
}

De manera que pueda ser llamado así:

DataTable dt = LeerArchivoPlano(
    new FileInfo(@"C:\DirectorioDeArchivosCSV\jason.csv"),
    true, TipoDeArchivoPlano.Delimited);

En fin, la idea es esa. Enjoy.

Usando using

El día de hoy aprendí un tip bastante bueno de como usar using gracias a Eber Irigoyen.

Todo comenzó porque él hizo un comentario en el último artículo recalcando la importancia de usar using para TODOS los objetos relacionados con la base de datos (conexiones, comandos, transacciones, etcétera). A mí se me hacía un poco de overkill, pero ahora comprendo que en realidad sí es un buen consejo.

Por si no sabes ni de qué hablo, esta instrucción define un alcance (scope) explícito para un objeto, fuera del cual el objeto es automáticamente "dispuesto" (manda llamar su método Close() o Dispose()), incluso si se levanta una excepción dentro de ese bloque.

Aunque estrictamente hablando no es necesario, ya que hay un scope implícito cada vez que abres y cierras llavecitas en C#, para recursos limitados como conexiones de red, el scope explícito que crea el using ayuda a que el garbage collector reclame esos recursos más fácil y rápidamente.

Si no lo tuvieras, tendrías que hacer todo a punta de bloques try/catch/finally. Eso era algo que no me gustaba de VB 7--bueno VB .NET o "VB 2003", como le quieran llamar. Afortunadamente VB 8 (2005) ya lo trae.

En el artículo original no la quise complicar mucho con esto, pero así quedaría el "patrón de uso de un DataReader" usando try/catch/finally:

string miStringDeConexion =
    @"Data Source=.\SQLEXPRESS;Initial Catalog=AdoNetDemo;" +
    "Integrated Security=True";
string miQuery = "SELECT * FROM Peliculas;";
 
SqlDataReader speedy = null;
SqlConnection miConexion = null;
SqlCommand miCommando = null;
try
{
    // en cualquiera de las instrucciones de este bloque se podría
    // levantar una excepción
 
    miConexion = new SqlConnection(miStringDeConexion);
    miCommando = new SqlCommand(miQuery, miConexion);
    miConexion.Open();
    speedy = miCommando.ExecuteReader();
    while (speedy.Read())
    {
        // hacer mi desmater 
    }
}
catch (SqlException)
{
    // hacer algo para intentar recuperarme o re-lanzar exception
// si no pienso hacer nada, es mejor omitir el catch
}
catch
{
    // hacer algo para intentar recuperarme o re-lanzar exception
// si no pienso hacer nada, es mejor omitir el catch
}
finally
{
    // haya o no haya ocurrido excepción, siempre debemos asegurarnos
    // de cerrar las conexiones, DataReaders, etc.
    if (!speedy.IsClosed)
        speedy.Close();
    if (miConexion != null)
        miConexion.Close();
    if (miCommando != null)
        miCommando.Dispose();
}

Como puedes ver, es medio engorroso. Por eso el using es tan cool.

Pero, yo tenía mis reservas, porque cuando tienes varios objetos, es fácil--pensaba yo--que acabes con un chorro de usings anidados, lo cual no me gustaba:

// asumiendo que TipoA, TipoB y TipoC son clases que
// implementan IDisposable
using (TipoA a = new TipoA())
{
    using (TipoB b = new TipoB())
    {
        using (TipoC c = new TipoC())
        {
            // hacer mi desmater
        } // using dispone c
    } // using dispone b
} // using dispone a

Ahora, también sabía que using soporta declarar varios objetos del mismo tipo dentro del paréntesis. Algo así como esto:

// De nuevo, asumiendo que TipoA implementa IDisposable
using (TipoA a1 = new TipoA(), a2 = new TipoA(), a3 = new TipoA())
{
    // hacer mi desmater
} // using dispone a1, a2 y a3

Lo cual no es muy útil cuando tienes objetos de distintos tipos, como en el patrón de uso de un DataReader--donde tienes que usar SqlCommand, SqlConnection y SqlDataReader.

Así que por eso se me hizo tan chido cuando Eber me comentó que using soporta una sintaxis más o menos así:

// asumiendo que TipoA, TipoB y TipoC son clases que
// implementan IDisposable
using (TipoA a = new TipoA())
using (TipoB b = new TipoB())
using (TipoC c = new TipoC())
{
   // hacer mi desmater
} // using dispone a, b y c

De esa forma podría re-escribir el ejemplo de esta forma:

string miStringDeConexion =
    @"Data Source=.\SQLEXPRESS;Initial Catalog=AdoNetDemo;" +
    "Integrated Security=True";
string miQuery = "SELECT * FROM Peliculas;";
 
using(SqlConnection miConexion = new SqlConnection(miStringDeConexion))
using(SqlCommand miCommando = new SqlCommand(miQuery, miConexion))
{
    miConexion.Open();
    using (SqlDataReader speedy = miCommando.ExecuteReader())
    {
        while (speedy.Read())
        { // hacer mi desmater 
        }
    } // using manda llamar speedy.Close()
} // using manda llamar miConexion.Close() y miComando.Dispose()

Ahora, nomás de payaso modifiqué un poco el código para verificar que en realidad se estuvieran mandando llamar los Dispose(), agregando manejadores para el evento Disposed tanto de la conexión como del comando de la siguiente forma:

public static void UsandoUsing() {
 
    string miStringDeConexion =
        @"Data Source=.\SQLEXPRESS;Initial Catalog=AdoNetDemo;" +
        "Integrated Security=True";
    string miQuery = "SELECT * FROM Peliculas;";
 
    using(SqlConnection miConexion = new SqlConnection(miStringDeConexion))
    using(SqlCommand miCommando = new SqlCommand(miQuery, miConexion))
    {
        miConexion.Disposed += new EventHandler(miConexion_Disposed);
        miCommando.Disposed += new EventHandler(miCommando_Disposed);
 
        miConexion.Open();
        using (SqlDataReader speedy = miCommando.ExecuteReader())
        {
            while (speedy.Read())
            { // hacer mi desmater 
            }
        } // using manda llamar speedy.Close()
    } // using manda llamar miConexion.Close() y miComando.Dispose()
}
 
static void miConexion_Disposed(object sender, EventArgs e)
{
    Console.WriteLine("miConexion liberada");
}
 
static void miCommando_Disposed(object sender, EventArgs e)
{
    Console.WriteLine("miComando liberado");
}

¿El resultado?
Oh yeah.

ADO.NET para novatos

ADO.NET es la tecnología de acceso a datos que viene en el .NET Framework. Y aunque su nombre proviene de su predecesor (ADO, ActiveX Data Objects), no tiene casi nada en común con esa tecnología. De hecho, maneja un paradigma conceptual completamente opuesto.

ADO, RDO, DAO y similares manejaban un modelo “conectado”, es decir, tu ejecutabas un query en la base de datos y declarabas objetos que mantenían un “cursor” o un apuntador al registro que querías leer. ADO te proveía métodos para navegar hacia enfrente o hacia atrás en el grupo de datos que te regresaba tu query para que pudieras leerlos y/o alterarlos. Todo esto se hacía en vivo y en directo sobre la base de datos. Las desventajas de esta tecnología eran algunas, pero la principal es que necesitas mantener una conexión abierta en todo momento para manipular los datos. Así que si tenías, digamos 20 licencias para conectarte a tu base de datos, pues fácilmente podías saturarlas con 20 usuarios que estuvieran utilizando la aplicación con alguna pantalla abierta, aunque no estuvieran realizando ninguna actividad.

Es como cuando tienes un vecino muy platicador y con muy poca vida propia. Llegas tu casa del trabajo y estás metiendo el auto en tu cochera, cuando te lo topas y lo saludas:
—¿Qué tal vecino, qué cuentas?
—Pos fíjate que la suegra del primo de la hermana de la vecina wachumaru la gallina blah blah blah blah blah bla…
(continuando por al menos 30 minutos).

Y tú ya te quieres meter a tu casa porque te estás cayendo de sueño, pero el maldito vecino no se calla y ya mejor no quieres ni hablar porque sabes que si le haces cualquier otra pregunta o haces la más mínima muestra de interés en la conversación vas a estar ahí otra hora y él todavía está hablando de lo gracioso que se ve su bebé recién nacido cuando regurgita y… ¡aaaAAARGHHH!

Ejem… ¿en qué estaba? ¡Ah sí!

Sin embargo, ADO.NET maneja un modelo “desconectado”, más que nada por que el otro no es el más óptimo en cierto tipo de aplicaciones como las de Web o dispositivos móviles. Es decir, la idea aquí es que te conectes a la base de datos (o cualquier otro almacén de datos), hagas lo que tengas que hacer y te desconectes, liberando lo más rápido posible recursos de red, licencias, etcétera. Puedes mantener si gustas una copia cacheada de los datos en memoria, y luego cuando estés conectado nuevamente simplemente sincronizas tus datos con los del servidor—más sobre esto cuando veamos los DataSets.

Así que en lugar del vecino guacamaya, es como cuando te topas un compa de toda la vida en la calle:
—¿Qué rollo, vato?
—Cero balero. ¿Qué cuentan las morritas?
—Nada, ahí andan todas bien felices.
—Órale. Qué buen pex. Me las saludas… nos vemos.
(Y ya, tan tan)

¿DataWhat?

En cuanto comienzas a trabajar con ADO.NET verás un montón de clases con nombres similares: DataReader, DataAdapter, DataSet, DataTable, DataRow, DataRowView, DataCongal… bueno ese último quizá no sea real, pero el resto de las clases sí existen y debes conocerlas si quieres utilizar tus DataDatos en tu DataAplicación. Está bueno pues, ya fueron suficientes DataPayasadas.

Pero antes de meternos en ese tema, hay otros dos objetos que son más primitivos y que permiten que te conectes e interactúes con una base de datos: Connection y Command.

Connection permite que entables un canal de comunicación con tu base de datos, y Command permite que ejecutes alguna instrucción sobre el motor de la base de datos, por ejemplo un query SQL de SELECT, INSERT, UPDATE o DELETE, o ejecutar un Stored Procedure.

Ilustración conceptual de Connection y Command.  Da clic para ver la imagen en tamaño normal.
La clase exacta que utilizarás para establecer tu conexión y comandos dependerá del proveedor de datos que utilices, y el proveedor dependerá del tipo de base de datos a la que te quieras conectar: SQL Server, Oracle, Access, MySQL, etcétera. Y aunque hay una forma en el .NET Framework 2.0 de hacer tu código “independiente del proveedor de base de datos”, no hay que mezclar el caldo con la sopa todavía; lo veremos en otro artículo.

Por ahora, si quieres interactuar con una base de datos SQL Server, tendrás que utilizar SqlConnection y SqlCommand; si quieres platicar con Oracle tendrás que usar OracleConnection y OracleCommand; y si quieres hacerlo con Access, puedes usar OleDbConnection y OleDbCommand. Sin importar cuál selecciones, los pasos para utilizarlo siempre son los mismos:
  1. Abres la conexión
  2. Haces tu desmadre (o sea, ejecutas tus comandos)
  3. Cierras la conexión
Usando SQL Server sería algo como esto:

// asumiendo que al principio se declaró
// using System.Data.SqlClient; 
// para usar más fácilmente clases del SQL Server Data Provider
 
SqlConnection miConexion = new SqlConnection("MiStringDeConexion");
miConexion.Open();
 
// Hago mis cosas
 
miConexion.Close();

La conexión requerirá de un string de conexión, de acuerdo al proveedor de datos, para indicar a qué base de datos conectarse y con qué credenciales. Por ahora utilizaré "MiStringDeConexión" para mostrar donde va ese valor.

Una práctica muy recomendada para evitar que se nos olvide cerrar la conexión es utilizar la instrucción using de C# (VB 2005 también la tiene). Ojo: No es la misma que la que se utiliza para importar un espacio de nombres.

using (SqlConnection miConexion = new SqlConnection("MiStringDeConexion"))
{
    miConexion.Open();
    // Hago mis cosas
 
    // using, automáticamente invoca los finalizadores, Close() en este caso,
    // en cuanto se termina el scope de la instrucción.
}

El comando en sí se especifica utilizando la propiedad CommandText. Además, puedes especificar el tipo del comando que estás ejecutando con la propiedad CommandType:
  • Text—Cualquier query, incuyendo comandos para manipular la base de datos (CREATE TABLE, DROP TABLE, etc.)
  • StoredProcedure—Para ejecutar un procedimiento almacenado.
  • TableDirect—Para leer una tabla completita. Este no es soportado por algunos proveedores de datos como el de SQL Server.
using (SqlConnection miConexion = new SqlConnection("MiStringDeConexion"))
{
    miConexion.Open();
    //
    // hago mis cosas:
    //
    SqlCommand cmd1 = new SqlCommand();
    cmd1.Connection = miConexion;
    cmd1.CommandType = System.Data.CommandType.Text;
    cmd1.CommandText = "SELECT * FROM 'Peliculas'";
    // no lo estamos ejecutando todavia
 
    SqlCommand cmd2 = new SqlCommand();
    cmd2.Connection = miConexion;
    cmd2.CommandType = System.Data.CommandType.StoredProcedure;
    cmd2.CommandText = "SelectPeliculas"; // nombre de mi SP en mi DB
    // falta ejecutarlo
 
} // using manda llamar miConexion.Close()

Para ejecutar tus instrucciones, la clase Command te da 3 métodos principales:
  • ExecuteNonQuery()—Para cuando tu instrucción no regresa una respuesta, p. ej. un INSERT, UPDATE o DELETE
  • ExecuteScalar()—Para cuando tu instrucción regresa UN SOLO registro con UN SOLO campo con un valor numérico, por ejemplo el resultado de un “SELECT COUNT(*) FROM MiTabla”
  • ExecuteReader()—Para cuando quieres leer el resultado de uno o varios SELECTs usando un DataReader.
// la práctica recomendada es alamacenar este string de conexión
// en el archivo .config de mi aplicación (web.config o app.config).
//
// en este ejemplo, me estoy conectando a la instancia local 
// SQLEXPRESS, a una base de datos llamada AdoNetDemo utilizando 
// las credenciales del usuario que ejecuta mi aplicación
string miStringDeConexion =
    @"Data Source=.\SQLEXPRESS;Initial Catalog=AdoNetDemo;" +
    "Integrated Security=True";
 
using (SqlConnection miConexion = new SqlConnection(miStringDeConexion))
{
    miConexion.Open();
 
    // un INSERT utilizando SQL
    int registrosAfectados = 0;
    string miQuery =
      "INSERT INTO Peliculas (Titulo, FechaDeAdquisicion, Precio) " +
      "VALUES ('El Santo y Blue Demon vs. las Vampiras','2006-08-06',15.65);";
 
    SqlCommand cmd = new SqlCommand(miQuery, miConexion);
    cmd.CommandType = System.Data.CommandType.Text;
 
    registrosAfectados = cmd.ExecuteNonQuery();
    // registrosAfectados ahora tiene valor de 1;
 
 
    // ahora un SELECT COUNT(*) usando SQL
    int numDeRegistros = 0;
    string otroQuery = "SELECT COUNT(*) FROM Peliculas;";
 
    SqlCommand comandoParaContar = new SqlCommand(otroQuery, miConexion);
    numDeRegistros = Convert.ToInt32(comandoParaContar.ExecuteScalar());
 
 
    // finalmente, un ejemplo de como leer todos los registros de una tabla
    SqlCommand miCommando =
        new SqlCommand("SELECT * FROM Peliculas;", miConexion);
 
    SqlDataReader speedy;
    speedy = miCommando.ExecuteReader();
} // using cerrará la conexión

¡Un momento! ¡Ya salió el DataReader!

Supongo entonces, que sería buena idea hablar de ellos.

DataReaders y DataSets
Cuando quieres leer datos, tienes de dos sopas en .NET: usar un DataReader o usar un DataSet con un DataAdaper. Para ambos necesitarás una Connection y al menos un Command (para el SELECT), pero la diferencia está en qué puede hacer uno y qué puede hacer otro.

DataReader es un objeto ligero y súper rápido que te permite leer UN registro a la vez, leyendo hacia enfrente solamente. Es lo que se conoce como un firehose reader, porque avienta los datos como si fuera una manguera a presión. En otras palabras DataReader es como Speedy Gonzáles: chiquito, prieto y medio feo pero bien rápido el desgracia’o.

DataReader vs. DataSet. Da clic para ver la imagen en tamaño normal.
Por otro lado, el DataSet es como Porky Pig: medio ghey y más choncho—porque contiene algunos sub-objetos—pero que puede hacer más graciosadas. Con esos nos vamos a clavar en unos momentos más (sin albur), pero primero vamos a ver el patrón clásico para utilizar un DataReader:

Ilustración conceptual del uso de un DataReader. Da clic para ver la imagen en tamaño normal.
Que en código se vería más o menos como lo siguiente:

// declarar conexion y comando;
// declarar DataReader;
// "abrir" el DataReader con el método ExecuteReader() del comando;
// leer los datos, uno por uno:
while (reader.Read())
{
    // hacer algo con los datos que leo
}
// cerrar el DataReader

El método Read() se encarga de leer los datos y al mismo tiempo, mover el “cursor” al siguiente registro. El método regresa true si logró leer el siguiente o false si ya no hay más registros que leer.

A pesar de ser sencillo y eficiente, el DataReader tiene algunas “desventajas” como son:
  • Es solo-lectura. Si quieres actualizar datos tendrías que ejecutar un Command por separado.
  • No te dice información sobre los datos que estás leyendo, por lo que no solo tienes que conocer el tipo de datos que estás leyendo, sino el orden de las columnas.
Suponiendo que tuviéramos la siguiente tabla con la información de tu colección de películas:

Da clic para ver la imagen en tamaño normal.
Tendrías que leerlos así:

string miStringDeConexion =
    @"Data Source=.\SQLEXPRESS;Initial Catalog=AdoNetDemo;" +
    "Integrated Security=True";
 
using (SqlConnection miConexion = new SqlConnection(miStringDeConexion))
{
    // variables para poner los valores de cada columna
    int id = 0;
    string titulo = string.Empty;
    DateTime fechaDeAdquisicion;
    Decimal precio = 0;
 
    miConexion.Open();
 
    SqlCommand miCommando =
        new SqlCommand("SELECT * FROM Peliculas;", miConexion);
 
    using (SqlDataReader speedy = miCommando.ExecuteReader())
    {
        while (speedy.Read())
        {
            id = speedy.GetInt32(0);      // la primer columna es el ID
            titulo = speedy.GetString(1); // la segunda columna es el Titulo
            fechaDeAdquisicion = speedy.GetDateTime(2); // etc...
            precio = speedy.GetDecimal(3);
 
            // hacer algo con los valores leidos.
            //
            // las variables ahora tienen el valor del registro actual
            // en esta iteración.
        }
    } // using cerrará el DataReader
} // using cerrará la conexión

Así que si lo único que necesitas es leer datos de manera eficiente, Speedy… digo… el DataReader es el que te conviene.

Pero habrá ocasiones en las que tengas que manipular varios datos a la vez, o necesites ver la relación de datos entre varias tablas. Es ahí cuando los DataSets y los DataTables son bastante útiles.

Ilustración conceptual del uso de un DataAdapter con un DataSet. Da clic para ver la imagen en tamaño normal.
Un DataSet puede contener una o varias DataTable que representen datos de tu base de datos. Cada DataTable, tiene una colección de DataRows que representan los registros que fueron leídos.

El papel del DataAdapter es el de “sincronizador de datos” por medio de sus dos métodos principales, Fill() y Update(). La forma de trabajar sería:
  1. Llenas tu DataSet
  2. Haces tu desmadre—actualizas, insertas, borras registros
  3. Actualizas tu base de datos
Una ventaja al usar un DataAdapter es que no tengo que preocuparme por andar abriendo y cerrando la conexión. Estos métodos lo hacen automáticamente.

Con Fill() puedes llenar todo tu DataSet—o una DataTable específica de tu DataSet—con los datos de la base de datos. Para usarlo, debes primero configurarle al DataAdapter su SelectCommand, que es el que indica qué query debe ejecutar para traerse los datos. Si no planeas actualizar datos, pues este es el único comando que necesitas configurarle.

// asumiendo que al principio se declaró
// using System.Data.SqlClient; 
// para usar más fácilmente clases del SQL Server Data Provider y
// using System.Data;
// para usar DataSet, DataTable, DataRow, et. al.
 
string miStringDeConexion =
    @"Data Source=.\SQLEXPRESS;Initial Catalog=AdoNetDemo;" +
    "Integrated Security=True";
 
SqlConnection miConexion = new SqlConnection(miStringDeConexion);
 
SqlCommand miCommando =
        new SqlCommand("SELECT * FROM Peliculas;", miConexion);
 
SqlDataAdapter miAdaptador = new SqlDataAdapter(miCommando);
// tambien habriamos podido utilizar la propiedad SelectCommand
// miAdaptador.SelectCommand = miComando;
 
DataSet porky = new DataSet();
 
// llenar mi DataSet de acuerdo al SelectCommand
miAdaptador.Fill(porky);
 
// el DataAdapter automáticamente creó un DataTable en mi DataSet
// con la misma estructura que el resultado de mi query,
// incluyendo nombres de columnas y tipos de datos
//
// ahora puedo leer los registros de la siguiente manera:
foreach (DataRow registro in porky.Tables[0].Rows) 
{
    //registro["ID"];
    //registro["Titulo"];
    //registro["FechaDeAdquisicion"];
    //registro["Precio"];
}

La manera más sencilla de comprobar que mi DataSet o DataTable tiene ya datos es utilizando el visualizador que viene con Visual Studio:

Imagen de cómo invocar el DataSet Visualizer. Da clic para ver la imagen en tamaño normal.Imagen del DatSet visualizer. Da clic para ver la imagen tamaño normal.
Con Update() el DataAdapter intenta propagar los cambios que hayas hecho en tu DataSet hacia la base de datos.

¿Cómo lo hace? Polvos de hada.

Bueno, en realidad usa una propiedad de cada DataRow llamada RowState, que puede tener valores como “nuevo”, “sin cambios”, “modificado” y “eliminado”. El estado de cada DataRow se ajusta en cuanto haces algún cambio a los datos. De esa forma lo único que tiene que hacer el DataAdapter, cuando llamas Update() es recorrer todos los rows del DataSet e invocar el InsertCommand, UpdateCommand o DeleteCommand, de acuerdo a si el DataRow es nuevo, modificado o eliminado, respectivamente. Obviamente para que esto funcione, necesitas haberle configurado previamente esos comandos al DataAdapter.

Aunque podrías especificarlos manualmente, si quieres evitar la hueva errores al hacerlo, puedes utilizar un CommandBuilder, que automáticamente los inferirá—el Insert, Update y DeleteCommand—en base al SelectCommand.

string miStringDeConexion =
    @"Data Source=.\SQLEXPRESS;Initial Catalog=AdoNetDemo;" +
    "Integrated Security=True";
 
SqlConnection miConexion = new SqlConnection(miStringDeConexion);
 
SqlCommand miCommandoSelect =
        new SqlCommand("SELECT * FROM Peliculas;", miConexion);
 
SqlDataAdapter miAdaptador = new SqlDataAdapter(miCommandoSelect);
 
SqlCommandBuilder miConstructor = new SqlCommandBuilder(miAdaptador);
 
DataSet porky = new DataSet();
 
// llenar mi DataSet de acuerdo al SelectCommand
miAdaptador.Fill(porky);
 
// modificar algunos de los registros
 
// al primer registro, ponerle el título en mayúsculas
porky.Tables[0].Rows[0]["Titulo"] = 
    porky.Tables[0].Rows[0]["Titulo"].ToString().ToUpper();
// eliminar el 4to registro
porky.Tables[0].Rows[3].Delete(); 
 
 
// actualizar la base de datos en base a los cambios
// hasta antes este momento la BD sigue intacta
miAdaptador.Update(porky);
 
// los cambios fueron propagados a la BD.

Los datos despúes de la actualización. Da clic para ver la imagen en tamaño normal.
Para ser un poco más completos, hay que mencionar que DataSet tiene muchas más monerías que no alcanzaremos a ver en este artículo. Por ejemplo, puede representar varias tablas de tu base de datos con todo y relaciones para mantener la integridad referencial.

Ilustración conceptual de un DataSet. Da clic para ver la imagen en tamaño normal.
Como en el siguiente ejemplo:

Un ejemplo de un DataSet visto en el designer de Visual Studio. Da clic para ver la imagen en tamaño normal.
En otras palabras, puedes representar toda o una parte de tu base de datos por medio de un DataSet. Sin embargo, cuando manejas varias tablas al mismo tiempo, las cosas comienzan a ponerse interesantes cuando quieres llenarlas o actualizarlas.

Quizá es por eso que en Visual Studio 2005—incluyendo las versiones Express—ahora trae soporte para TableAdapters, que será el tema del próximo artículo en esta serie.



Este es el primero de varios artículos que tratan sobre ADO.NET y las monerías que trae la versión 2.0 del .NET Framework. Si quieres averiguar más sobre el tema, haz una búsqueda en este blog o dale clic a la etiqueta de ado.net.

Si tienes sugerencias, correcciones u opiniones sobre este artículo o si tienes sugerencias para artículos nuevos de temas que te gustaría ver en este blog, deja un comentario mediante los enlaces de abajo.

Las imágenes y nombres de los personajes "Speedy Gonzales" y "Porky Pig" son propieadad de Warner Bros. y tomados sin permiso para fines de no-lucro. Así que si piensan demandarme, ni se molesten, porque saldría corriendo y gritando como Flanders para quitar las imágenes y referencias de este blog.

ADO.NET en los próximos días

La semana pasada me tocó meter mi cuchara (sin albur) en la plática de ADO.NET.

Luego el viernes en a las 5:00 PM el Pepe estaba histérico porque la persona que iba a dar el taller tuvo que cancelar de último momento. Así que me habló para que entrara de bateador emergente.

Llegué el sábado medio ojeroso porque sí me desvelé preparando el taller para que tuviera cosas interesantes. Y valió la pena (o al menos le gustó al Pelos).

Aunque no pudimos tratar muchas cosas que yo hubiera querido sobre ADO.NET 2.0--Provider Factories, serialización binaria de DataSets, mejoras a la clase DataTable, etc.--sí alcanzamos a ver algunas: las enormes mejoras en el databinding para Windows Forms con el BindingSource, los TableAdapters y muchas de las mejoras en Visual Studio, desde el Designer de los DataSets, el panel DataSources y hasta las herramientas para refactorizar y generar Stored Procedures automáticamente con el mismo IDE.

Y no faltaron los comentarios del Edgar, que cada vez que yo mostraba algo chido--según yo--él se encargaba de recordarnos "Uuuu pos Borland hace eso desde hace 3 años". Jejeje, demmet. En fin.

Nuevamente utilizamos una versión Express de Visual Studio, esta vez fue Visual C# 2005 Express Edition, para que la raza se diera cuenta de lo potente que están estas herramientas gratis.

Lo que me di cuenta con la plática y el taller es que mucha raza todavía anda buscando más detalles sobre el tema, así que ya comencé a escribir un artículo(te) en español al respecto, completo con varias ilustraciones, incluyendo las que dibujé el día de la plática.

Sí ya sé que probablemente haya algunos otros por ahí, pero ya ven que tengo mi modo de decir las cosas. El artículo resume todos los conceptos respecto a ADO.NET que considero importantes, desde lo básico. Espero tenerlo listo para este fin de semana.

También tengo planeado una serie de posts (o quizá otro artículo) para cubrir los nuevos features que trae la versión .NET 2.0 y que no pudimos tratar bien en la plática o el taller.

Pensé en hacer un screencast (o varios) al respecto, pero soy pobre y tengo que mantener a las teiboleras, digo... no tengo espacio en donde hostear esos archivos por el momento. Si alguien quiere volutariarlo pues lo agradecería. Aunque creo que digo tarugadas más divertidas cuando escribo...

Así que si no ven posts míos lo que queda de esta semana ya saben por qué. (Ando en el Premier)