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.

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:
- Abres la conexión
- Haces tu desmadre (o sea, ejecutas tus comandos)
- 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.

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:

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:

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.

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:
- Llenas tu DataSet
- Haces tu desmadre—actualizas, insertas, borras registros
- 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:


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.

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.

Como en el siguiente ejemplo:

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.