Anti Prácticas .NET: Lectura de Datos con ADO.NET II

Dada la sugerencia de Sergio Tarrillo, en este artículo mediremos la lectura de datos de una base de datos con ADO.NET, incluyendo la carga de una lista genérica List<> de objetos de entidad.Este artículo es una continuación del artículo anterior Anti Prácticas .NET: Lectura de Datos con ADO.NET

Presentación del escenario

Este es el contexto en el que estoy haciendo las mediciones:Una aplicación Windows Forms, que utiliza 3 mecanismos para recuperar datos “de solo lectura” de la base de datos AdvertureWorks alojada en SQL Server 2005: 

  • DataReader cargado en una lista genérica de objetos de entidad
  • DataSet
  • DataTable Aquí subrayo “solo lectura” porque, justamente solo quiero recuperar los datos, y no hacer ninguna operación sobre ellos.

El Código

La versión completa del código podrás bajarla de aquí.  De todas formas démosle un vistazo:
Esta es la sentencia sql a ejecutar en la base de datos AdventureWorks:

Select
HumanResources.Employee.EmployeeID, Person.Contact.FirstName
       Person.Contact.MiddleName, Person.Contact.LastName,
       HumanResources.Employee.Title, HumanResources.Employee.BirthDate,
       Person.Address.AddressLine1, Person.Address.AddressLine2,
       Person.Address.City, Person.Address.PostalCode, Person.Contact.EmailAddress,
       Person.Contact.Phone, HumanResources.Employee.MaritalStatus, HumanResources.Employee.Gender
       FROM HumanResources.Employee
       INNER JOIN  Person.Contact
          ON HumanResources.Employee.ContactID = Person.Contact.ContactID
       INNER JOIN HumanResources.EmployeeAddress
          ON HumanResources.Employee.EmployeeID = HumanResources.EmployeeAddress.EmployeeID
       INNER JOIN Person.Address
          ON HumanResources.EmployeeAddress.AddressID = Person.Address.AddressID
          AND  HumanResources.EmployeeAddress.AddressID = Person.Address.AddressID

y la clase DataAccess:

namespace Walzer.Antipracticas{
    public class DataAccess
    {
        static readonly string _connString;
        static readonly string _sqlCmd;

        static DataAccess()
        {
            _connString = “Password=;User ID=;Initial Catalog=AdventureWorks;Data Source=WALZER3”;
            //Obtengo la sentencia SQL que está en el archivo de texto Consulta.sql
            StreamReader sr = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream(“Walzer.Antipracticas.Consulta.sql”));
            _sqlCmd = sr.ReadToEnd();
        }

        static public DataSet TraerDataSet()
        {
            DataSet ds = null;
            try
            {
                using (SqlConnection conn = new SqlConnection(_connString))
                {
                    conn.Open();
                    SqlCommand cmd = new SqlCommand();
                    cmd.CommandText = _sqlCmd;
                    cmd.Connection = conn;
                    cmd.CommandType = CommandType.Text;
                    SqlDataAdapter da = new SqlDataAdapter(cmd);
                    ds = new DataSet();
                    da.Fill(ds);
                }
            }
            catch {}
            return ds;
        }

        static public List<Employee> TraerEmployees()
        {
            List<Employee> employees = new List<Employee>();
            try
            {
                using (SqlConnection conn = new SqlConnection(_connString))
                {
                    SqlCommand cmd = new SqlCommand();
                    cmd.CommandText = _sqlCmd;
                    cmd.Connection = conn;
                    cmd.CommandType = CommandType.Text;
                    conn.Open();
                    using (SqlDataReader dr = cmd.ExecuteReader())
                    {
                        while (dr.Read())
                        {
                            Employee employee = new Employee();
                            employee.EmployeeID = Convert.ToInt32(dr[“EmployeeId”]);
                            employee.FirstName = Convert.ToString(dr[“FirstName”]);
                            //Por motivos de espacio obvié las lineas restantes…
                            employees.Add(employee);
                        }
                    }
                }
            }
            catch {}
            return employees;
        }

       
static public DataTable TraerDataTableOptimizado()
        {
            //Este método está optimizado para cargar un DataTable con datos de SOLO LECTURA
            DataTable dt = null;
            try
            {
                using (SqlConnection conn = new SqlConnection(_connString))
                {
                    conn.Open();
                    SqlCommand cmd = new SqlCommand();
                    cmd.CommandText = _sqlCmd;
                    cmd.Connection = conn;
                    cmd.CommandType = CommandType.Text;
                    SqlDataAdapter da = new SqlDataAdapter(cmd);
                    dt = new DataTable();
                    da.Fill(dt);
                }
            }
            catch {}
            return dt;
        }
     }
} 

Comparación de Técnicas

Lo que primero vamos a medir es el tiempo que consume cada uno de las técnicas de lectura de datos. Para ello recordemos que la consulta devuelve 290 regitros y que ejecuto 10 veces cada método.

Podemos observar entonces que no hay mayor diferencia entre las técnicas.  Claro que ya hemos optimizado en el artículo anterior la lectura del DataTable.

Optimización de la carga de una lista genérica de objetos de entidad con DataReader

De todas formas sería bueno revisar el método TraerEmployees, que carga una List<Employee> para ver si le cabe alguna optimización.

 Encontramos aquí que tenemos 446 ms. + 74 ms. en 34800 + 5800 invocaciones a GetOrdinal() debido a la forma en que recuperamos el valor de la columna dr[“nombreCampo”]. GetOrdinal devuelve la posición de la columna para ese nombre campo, y esto es muy conveniente.  No es recomendable reemplazar el nombre de la columna por el número de la misma al recuperar el dato, ya que cualquier cambio en la consulta SQL invalidaría nuestro código.  Lo que debemos hacer entonces es minimizar la cantidad de llamadas. Quitemos la invocaciones a GetOrdinal() de adentro del bucle.

static public List<Employee> TraerEmployeesOptimizado1()
{
    List<Employee> employees = new List<Employee>();
    try
    {
        using (SqlConnection conn = new SqlConnection(_connString))
        {
            SqlCommand cmd = new SqlCommand();
            cmd.CommandText = _sqlCmd;
            cmd.Connection = conn;
            cmd.CommandType = CommandType.Text;
            conn.Open();
            using (SqlDataReader dr = cmd.ExecuteReader())
            {
                int colEmployeeId = dr.GetOrdinal(“EmployeeId”);
                int colFirstName = dr.GetOrdinal(“FirstName”);
                //Por motivos de espacio obvié las lineas restantes…
                while (dr.Read())
                {
                    Employee employee = new Employee();
                    employee.EmployeeID = Convert.ToInt32(dr[colEmployeeId]);
                    employee.FirstName = Convert.ToString(dr[colFirstName]);
                    //Por motivos de espacio obvié las lineas restantes…
                    employees.Add(employee);
                }
            }
        }
    }
    catch {}
    return employees;
}

Revisemos el profiling de código del código optimizado:

Hemos bajado el tiempo de 2631 ms. a 2132 ms. y reducido las invocaciones a GetOrdinal() a las necesarias: 14 invocaciones -> 14 campos.La segunda reducción que podríamos hacer corresponde con get_MetaData(), la cual se invoca por registro y por columna. La intención de minimizar esta llamada es la siguiente: ¿Para qué leer la “metadata” una y otra vez si el esquema de los datos no varía en un set de resultados de solo lectura como es el DataReader? ¿Cómo podemos hacer para reducir la cantidad de invocaciones?. Bien, mi sugerencia es tratando de reducir las invocaciones a GetValue() que internamente llama a get_MetaData(), reemplazándola por GetValues(object[]), la cual lee los datos del registro de una vez y devuelve un vector con los resultados.

static public List<Employee> TraerEmployeesOptimizado2()
{
    List<Employee> employees = new List<Employee>();
    try 
  
{
        using (SqlConnection conn = new SqlConnection(_connString))
        {
            SqlCommand cmd = new SqlCommand();
            cmd.CommandText = _sqlCmd;
            cmd.Connection = conn;
            cmd.CommandType = CommandType.Text;
            conn.Open();
            using (SqlDataReader dr = cmd.ExecuteReader())
            {
                int colEmployeeId = dr.GetOrdinal(“EmployeeId”);
                int colFirstName = dr.GetOrdinal(“FirstName”);
                //Por motivos de espacio obvié las lineas restantes…
                int colCount = dr.FieldCount;
                object[] values = new object[colCount];
                while (dr.Read())
                {
                    Employee employee = new Employee();
                    dr.GetValues(values);
                    employee.EmployeeID = Convert.ToInt32(values[colEmployeeId]);
                    employee.FirstName = Convert.ToString(values[colFirstName]);
                    //Por motivos de espacio obvié las lineas restantes…
                    employees.Add(employee);
                }
            }
        }
    }
    catch {}
    return employees;
}

Si observamos ahora el profiler vemos que las invocaciones a get_MetaData() se hacen por registro y no por columna.

Con esta segunda optimización, hemos logrado reducir el tiempo 2631 ms. a 1883 ms., disminuyendo las invocaciones a get_MetaData() de 4060 a 290. Logramos entonces una reducción del 40%.

Conclusión

Hemos comprobado que el uso correcto de las técnicas de acceso a datos en ADO.NET nos permite lograr un mayor rendimiento en nuestras aplicaciones.  También hemos aprendido algo de cómo funciona internamente ADO.NET, ejercicio que nos va a servir para tomar buenas decisiones al momento de elegir nuestra estrategia de acceso a datos.

Queda para una tercera entrega ver el costo de acceder a los datos usando los Asistentes de Visual Studio 2005.  Allí haré un resumen de lo investigado, para que tengan en cuenta al momento de diseñar su estrategia de acceso a datos.

Continuación

Parte III

10 thoughts on “Anti Prácticas .NET: Lectura de Datos con ADO.NET II

  1. Muy buen Articulo, he visto muchas maneras de Materializar Datos mediante DataReaders, esta a mi parecer y con pruebas 😛 parece ser la mas eficiente, muchas gracias por el articulo 🙂

  2. Carlos quiero agradecerte por tan magnifico articulo, ya que nos permites hacer analisis más técnicos sobre el funcionamiento de la arquitectuta ado.net.

  3. Bueno…yo hice esto, por que hace falta la validacion:

    if(!reader.IsDBNull(reader.GetOrdinal(“BillKey”)))
    bill.BillKey = Convert.ToInt64(values[reader.GetOrdinal(“BillKey”)]);

    Espero sus comentarios

Leave a Reply

Your email address will not be published. Required fields are marked *