Ir al contenido principal

Test 3: ¿Son eficientes los ORMs?

Bienvenidos amigos. Me complace anunciar que por fin estrenamos la categoría "SQL Server Tips", y lo hacemos por todo lo alto, entrando de lleno en un aspecto altamente polémico entre programadores. ¿Es eficiente un ORM en los accesos a datos?

Ya conocen la filosofía de nuestros Tests, no vamos a teorizar demasiado, pero sí una pequeña base va a ser necesaria para conseguir una buena respuesta a nuestra pregunta. He leído un interesante  artículo de nuestros súper amigos de Deloitte (cuando usar ORM) argumentando que el uso o no de un ORM hay que decidirlo en relación a la complejidad de nuestro modelo de datos, y al rendimiento que requeriremos en nuestras soluciones, pero, ¿cuándo no deseamos el mejor rendimiento para nuestro software?

Lo cierto es que, como ya hemos visto, el ORM facilita mucho las cosas, aporta claridad al código, de eso no cabe duda, pero, ¿es eficiente? He ahí la cuestión.

Sobre este asunto vamos a poner a funcionar nuestros apreciados Test. Concretamente dos: en éste primero vamos a hacer una inserción más o menos masiva de información, probando diferentes técnicas. Y en un segundo test usaremos esos datos cargados para elaborar algunas consultitas de cierta complejidad, para ver cuál es la manera más eficiente de obtener la info. ¿Les parece bien?

Antes de entrar en materia no puedo dejar de recordarles que disponen del proyecto íntegro, libre, sin ataduras, compilando que es gerundio, AQUÍ.

También quisiera mencionar que, además de las cuestiones de acceso a datos ya mencionadas, otras dos cuestiones técnicas van a ser abordadas en este artículo:
  • Uso de la clase Random para la generación de números aleatorios.
  • Uso de Reflection para convertir una clase en un DataTable.

Cargando miles de registros

Vamos a hacer una carga masiva. Para ello buscaremos la manera de generar miles y miles de registros.

Se me ha ocurrido el siguiente ejemplo: un parte de horas anual de una empresa imaginaria. 100 trabajadores imaginarios informan de las horas trabajadas día a día, durante el año 2020. Esta empresa opera en un mercado de mentira en un mundo de fantasía, que se rige por las siguientes normas inventadas:
- NUNCA se trabaja los fines de semana.
- No hay vacaciones.
- La jornada laboral es de 8 horas de lunes a viernes.
- Eventualmente, los trabajadores pueden enfermar. Las horas de trabajo perdidas por una baja médica no se recuperan.
- Eventualmente los trabajadores pueden tener incidencias personales que repercuten a la baja en sus horas de trabajo. Éstas podrían ser recuperadas por el trabajador con horas extras, o no. Depende de la responsabilidad de cada uno. En todo caso las horas extras motivadas por una recuperación nunca serán más de 2 diarias.

Con estas premisas vamos a generar partes de horas como churros. El año 2020 en estas condiciones tendría 262 días laborales (todos excepto los fines de semana), de ahí nuestros 26.200 registros.

Como no me apetece ahora mismo escribir a mano toda esta información, vamos a establecer un sistema de probabilidades, y usando la clase Random generaremos información en base a ese sistema probabilista. Se lo describo:

Si no hubo ninguna incidencia ni baja los últimos 5 días trabajados:
85% probabilidad de jornada normal
7% incidencia de 1 a 2 horas
5% incidencia de 2 a 4 horas
3% probabilidad de baja médica

Si hubo incidencia los últimos 5 días (sin baja médica en los últimos 5 días):
40% probabilidad de recuperar el 50% de la incidencia acumulada (max horas extras = 2)
20% probabilidad de recuperar el 100% de la incidencia acumulada (max horas extras = 2)
30% probabilidad jornada normal
4% probabilidad nueva incidencia entre 1 a 2 horas
3% probabilidad nueva incidencia entre 2 a 4 horas
3% probabilidad de baja médica
                     
Si hubo baja médica en los últimos 5 días:
80% probabilidad jornada normal
5%  probabilidad incidencia de 1 a 2 horas
5%  probabilidad incidencia de 2 a 4 horas
10% probabilidad nueva baja médica

Bien. Está claro que la cosa es irreal y ficticia. No quiero complicarlo más porque al fin y al cabo lo que queremos es generar registros más o menos coherentes, en grandes cantidades pero con ciertas variabilidades.

Vamos a separar lo que es la generación de los datos, del cálculo de eficiencia de los testcases con las distintas técnicas de acceso a datos, como enseguida podrán ver. De modo que los pasos a seguir serán:

1. TRUNCATE TABLE
2. Generar datos.
3. Test Case 1 - Insertar datos
4. TRUNCATE TABLE
5. Test Case 2 - Insertar datos
etc.

Generando los partes de horas

Vamos ya con el código. El primer paso es generar tanto la clase de dominio como el modelo de EF:

public class parte_horas
{
    public enum enumTipoJornada { Normal, Baja, Incidencia };

    public Guid Trabajador { get; set; }
    public DateTime Fecha { get; set; }

    public Int16 _horas;
    public Int16 Horas
    {
        get => _horas;
        set
        {
            _horas = value;

            if (Horas == 0)
            {
                this.TipoJornada = enumTipoJornada.Baja;
            }
            else if (Horas == 8)
            {
                this.TipoJornada = enumTipoJornada.Normal;
            }
            else
            {
                this.TipoJornada = enumTipoJornada.Incidencia;
            }
        }
    }

    public enumTipoJornada TipoJornada { get; set; }
}

[Table("ParteHoras", Schema = "test")]
public class ParteHoras
{
    public Int64 id { get; set; }

    [Required]
    public Guid Trabajador { get; set; }

    [Required]
    public DateTime Fecha { get; set; }

    [Required]
    public Int16 Horas { get; set; }
}

Solo un par de apreciaciones: como es habitual, estas dos clases son bastante parecidas en su estructura. Pero noten la diferencia en el campo TipoJornada. No está presente en el modelo porque no necesitamos consolidarlo en BBDD, ya que está directamente relacionado con el campo Horas, y de hecho su valor se establece de forma automática al establecer el valor de este último. Lo usamos tan solo durante la lógica del cálculo de partes -que enseguida veremos-, para aportarle al código una mejor legibilidad.

La segunda curiosidad es el adorno que tiene el modelo ParteHoras en su definición de clase ([Table("ParteHoras", Schema = "test")]). Sirve para que la tabla en base de datos se cree bajo un esquema distinto al esquema por defecto dbo, lo cual a su vez me permite distinguir los objetos que se usan para el funcionamiento mismo de la plataforma, de aquellos que se utilizar para cada uno de los test. Cuestión de orden, pero con implicaciones interesantes también en materia de seguridad. Decirles que con EF Code First el mero hecho de adornar el modelo de este modo crea el esquema en base de datos si éste no existiera previamente.

A continuación, y después de añadir el modelo en le context de EF, creamos la clase que generará el parte de horas en la carpeta functions, junto con la clase que calcula la serie fibo, o aquella que concatena los fragmentos de El Quijote, en nuestra capa de negocio:


Hereda las propiedades de la clase de dominio parte_horas, y contiene el método Generate() que vemos a continuación:

public List<ZmLabsObjects.sqltests.parte_horas> Generate(int numTrabajadores, int Anho)
{
    for (int cont = 0; cont < numTrabajadores; cont++)
    {
        //monta un hilo para calcular el parte anual de cada trabajador
        Guid _Trabajador = Guid.NewGuid();

        EstadoProceso.Add(new parte_horas_estado_proceso()
        {
            Trabajador = _Trabajador,
            EstadoProceso = parte_horas_estado_proceso.enumEstadoProceso.Ejecutando
        });

        Thread _thCalculoTrabajador = new Thread(() => CalculaParteAnualTrabajador(_Trabajador, Anho));

        _thCalculoTrabajador.Start();

        Thread.Sleep(75);
    }

    //espera al final del proceso
    while (EstadoProceso.Exists(est => est.EstadoProceso != parte_horas_estado_proceso.enumEstadoProceso.Finalizado))
    {
        Thread.Sleep(555);
    }

    return ParteAnual;
}

Como se puede intuir, vamos a delegar en un hilo de ejecución, el cálculo anual de cada uno de los trabajadores. De modo que este método no hace mucho más que generar dichos hilos de ejecución por cada trabajador. Esto es el método CalculaParteAnualTrabajador()

Verán que cada trabajador quedará representado por un GUID único, dando solidez a la queja recurrente de que las empresas consideran a sus trabajadores un número. En ésta al menos es un GUID.

Una vez lanzados todos los hilos, el método queda a la espera de la finalización de cada uno de ellos, antes de devolver el control al método Start() que lo invocó. En ese momento la lista de objetos de dominio que devuelve debería estar completamente informada.

public static void CalculaParteAnualTrabajador(Guid _Trabajador, int Anho)
{
    try
    {
        DateTime dtActual = new DateTime(Anho, 1, 1);
        DateTime dtFin = new DateTime(Anho, 12, 31);

        while (dtActual <= dtFin)
        {
            if (dtActual.DayOfWeek != DayOfWeek.Saturday & dtActual.DayOfWeek != DayOfWeek.Sunday)
            {
                ZmLabsObjects.sqltests.parte_horas _partediario = new ZmLabsObjects.sqltests.parte_horas()
                {
                    Trabajador = _Trabajador,
                    Fecha = dtActual
                };

                // ¿Hubo baja los últimos 5 días trabajados?
                bool HuboBaja = ParteAnual.Exists(r => r.Fecha >= dtActual.AddDays(-5) && r.TipoJornada == enumTipoJornada.Baja);

                // ¿Hubo incidencia los últimos 5 días trabajados, sin baja médica?
                // Si hubo obtiene las horas de incidencia acumuladas
                bool HuboIncidencia = false;
                int HorasIncidencia = 0;

                if (!HuboBaja)
                {
                    var Indicencias5Dias = ParteAnual.Where(r => r.Fecha >= dtActual.AddDays(-5) && r.TipoJornada == enumTipoJornada.Incidencia).ToList();
                    HuboIncidencia = Indicencias5Dias.Count > 0;

                    if (HuboIncidencia)
                    {
                        HorasIncidencia = Indicencias5Dias.Sum(h => 8 - h.Horas);
                    }
                }

                //calculamos las probabilidades para cada casuística
                if (HuboBaja)
                {
                    _partediario.Horas = GetHorasConBaja();
                }
                else if (HuboIncidencia)
                {
                    _partediario.Horas = GetHorasConIncidencia(HorasIncidencia);
                }
                else
                {
                    _partediario.Horas = GetHorasNormal();
                }

                ParteAnual.Add(_partediario);
            }


            dtActual = dtActual.AddDays(1);
        }

        EstadoProceso.Where(tr => tr.Trabajador == _Trabajador).First().EstadoProceso = parte_horas_estado_proceso.enumEstadoProceso.Finalizado;
    }
    catch (Exception ex)
    {
        string err = ex.Message;
    }
}

Vean este hermoso método. Concentra buena parte de la lógica del cálculo del parte diario de horas. Lo primero que hace es recorrer los días del año que estamos calculando. En primera instancia comprueba si el día en cuestión es sábado o domingo, en cuyo caso pasa a la siguiente iteración sin hacer nada.

Si es un día laborable, procede a verificar nuestros condicionantes lógicos, esto es, si hubo baja médica en los últimos 5 días, si hubo incidencias en los últimos 5 días o si no se dio en este periodo  ninguna de estas dos circunstancias.

En función de esta información establece las horas para el día en curso, llamando alternativamente a las funciones GetHorasConBaja(), GetHorasConIncidencia() o GetHorasNormal(), que albergarán la lógica de probabilidades de cada uno de los supuestos.

Las tres son muy parecidas, solo varían en los % de probabilidad. GetHorasConIncidencia() tiene además una pequeña lógica adicional para el tema de la recuperación de las horas, como vamos a ver:

private static Int16 GetHorasConBaja()
{
    int horas = 0;
    int rnd100 = _rnd.Next(1, 100);

    if (rnd100 <= 80)
    {
        //normal
        horas = 8;
    }
    else if (rnd100 > 80 && rnd100 <= 85)
    {
        //incidencia de 1 a 2 horas
        horas = 8 - _rnd.Next(1, 2);

    }
    else if (rnd100 > 85 && rnd100 <= 90)
    {
        //incidencia de 2 a 4 horas
        horas = 8 - (_rnd.Next(2, 4));
    }
    else
    {
        horas = 0;
    }

    return (Int16)horas;
}

private static Int16 GetHorasConIncidencia(int HorasIncidencia)
{
    int horas = 0;
    int rnd100 = _rnd.Next(1, 100);

    if (rnd100 <= 40)
    {
        //recupera el 50% de la incidencia acumulada (max horas extras = 2)
        var _50PctHotasIncidencia = Math.Round((decimal)HorasIncidencia / 2);

        horas = (Int16)(_50PctHotasIncidencia < 2 ? 8 + _50PctHotasIncidencia : 10);
    }
    else if (rnd100 > 40 && rnd100 <= 60)
    {
        //recupera el 100% de la incidencia acumulada (max horas extras = 2)
        horas = (Int16)(HorasIncidencia < 2 ? 8 + HorasIncidencia : 10);

    }
    else if (rnd100 > 60 && rnd100 <= 90)
    {
        //incidencia de 2 a 4 horas
        horas = 8;
    }
    else if (rnd100 > 90 && rnd100 <= 94)
    {
        //incidencia de 1 a 2 horas
        horas = 8 - _rnd.Next(1, 2);
    }
    else if (rnd100 > 94 && rnd100 <= 97)
    {
        //incidencia de 2 a 4 horas
        horas = 8 - (_rnd.Next(2, 4));
    }
    else
    {
        //baja médica
        horas = 0;
    }

    return (Int16)horas;
}

private static Int16 GetHorasNormal()
{
    int horas = 0;
    int rnd100 = _rnd.Next(1, 100);

    if (rnd100 <= 85)
    {
        //normal
        horas = 8;
    }
    else if (rnd100 > 85 && rnd100 <= 92)
    {
        //incidencia de 1 a 2 horas
        horas = 8 - _rnd.Next(1, 2);

    }
    else if (rnd100 > 92 && rnd100 <= 97)
    {
        //incidencia de 2 a 4 horas
        horas = 8 - (_rnd.Next(2, 4));
    }
    else
    {
        //baja médica
        horas = 0;
    }

    return (Int16)horas;
}

No voy a entrar en demasiadas explicaciones porque creo que el código se explica por si solo. Genera un número aleatorio del 1 al 100 para calcular el número de horas trabajadas en función de nuestro rango de probabilidades pre-establecido.

Bien, quiero recordarles que lo visto hasta aquí no forma parte ni computa en los tiempos de ningún testcase, sino que es una actividad previa para generar información. Para 100 trabajadores en el año 2020 se generan 26.2000 registros, suficientes para hacer las mediciones que pretendemos.

Vamos a entrar ahora en cuál es la mejor manera de mandarle estos 26.200 registros a nuestra base de datos.

Grabando miles de registros con Entity Framework

Ya en publicaciones anteriores hemos visto la estructura de los test. Vamos a recordar brevemente que cada uno de los test, en este caso la clase test3_sql_loaddata, es una clase derivada de test_exec que contiene los métodos y funciones comunes, y el método virtual Start() que es sustituido en las clases derivadas.

public override void Start()
{
    //inicia test
    this.InitTest();

    //limpia la tabla antes de iniciar la ejecución
    functions.parte_horas _partehoras = new functions.parte_horas();
    bool resTruncate = data_labs.ExecScript("truncate table [test].[ParteHoras]", _df.GetLabsCnx());

    //genera el parte de horas (fuera del cálculo de cada uno de los testcases)
    DateTime dtInicioCalculo = DateTime.Now;

    var listahoras = _partehoras.Generate(100, 2020);

    DateTime dtFinCalculo = DateTime.Now;

    TimeSpan _ts = dtFinCalculo - dtInicioCalculo;

    SetMsg("Cálculo anual 2020 para 100 trabajadores finalizado en " + _ts.TotalMilliseconds.ToString() + " milisegundos");
    SetMsg("- - - - -");

    //recorre y ejecuta testcases
    int cont = 0;

    while (cont < _testobject.TestCases.Count)
    {
        TestCases _test = _testobject.TestCases[cont];

        switch (_test.Function)
        {
            //Graba con EF
            case "EFBulkData":

                _testobject.TestCases[cont] = EFBulkData(listahoras, _test);

                //Limpia la tabla para nueva ejecución
                resTruncate = data_labs.ExecScript("truncate table [test].[ParteHoras]", _df.GetLabsCnx());

                break;

            case "ADOBulkData_Datatable":

                //Graba con Sp y Datatable
                _testobject.TestCases[cont] = ADOBulkData_Datatable(listahoras, _test);
                break;
        }

        cont++;
    }

    //finaliza test
    this.EndTest();
    _partehoras.Clear();
}

¿Qué particularidades apreciamos en este método Start()? Bueno, en primer lugar al iniciar el test debemos borrar en base de datos la información de la tabla de destino, ya que al final de la ejecución dejamos ahí los registros. Y debemos repetir esta operación entre la ejecución de un testcase y otro, para que las condiciones de partida sean las mismas.

Llamamos al método _partehoras.Generate() antes de iniciar la ejecución de los testcases. Este método obtiene una lista con nuestros 26.200 elementos, del objeto de dominio ParteHoras. Esta lista será usada en cada uno de los testcases.

A modo de curiosidad incluimos en los mensajes el tiempo de cálculo de los partes de horas, por si a posteriori nos apetece divertirnos mejorando este cálculo, pero no es el objeto del test.

Por último al finalizar el test añadimos una llamada a _partehoras.Clear() que vacía la variable interna que fue almacenando los resultados, quedando todo en un estado en que el test puede ser ejecutado de nuevo.

Por lo demás la estructura es igual que la del resto de los test, con el registro de inicio, el recorrido de los testcases asociados y el registro de fin. Veamos ahora la implementación del primer testcase, la grabación de nuestros 26.200 registros en BBDD mediante Entity Framework.

private TestCases EFBulkData(List<parte_horas> _ParteAnual, TestCases _test)
{
    TestCaseExecutions _testexec = new TestCaseExecutions() { idTestCase = _test.id };

    //registra inicio
    _testexec.dtBegin = DateTime.Now;
    InitTestCase(_test.Function, _testexec.dtBegin);

    //ejecuta testcase
    sqltest_repos_partehoras _testrepos = new sqltest_repos_partehoras(_df.GetLabsCnx());

    _testrepos.InsertParteHorasAnual(_ParteAnual);

    //registra fin
    _testexec.dtEnd = DateTime.Now;
    EndTestCase(_test.Function, _testexec);

    return _test;
}

En el método que implementa el testcase nos encontramos con la misma estructura que ya hemos comentados en test anteriores, esto es, la construcción del objeto TestCaseExecutions, la propia ejecución del test y el registro de fin.

La ejecución se limita a la invocación de un método, dentro de una clase de repositorio de Entity Framework, que recibe como parámetro la lista de registros en formato clase de dominio. El método se llama InsertParteHorasAnual, y contiene el código de la inserción de los registros en BBDD:

public bool InsertParteHorasAnual(List<ZmLabsObjects.sqltests.parte_horas> _ParteAnual)
{
    try
    {
        List<EFModels.testModels.ParteHoras> _ParteAnualModel = new List<EFModels.testModels.ParteHoras>();

        _ParteAnualModel = mapper.Map(_ParteAnual, _ParteAnualModel);

        using (var db = new context.LabsContext(_str_cnx))
        {
            db.ParteHoras.AddRange(_ParteAnualModel);
            db.SaveChanges();
        }
    }
    catch (Exception ex)
    {
        return false;
    }

    return true;
}

Como pueden ver es sumamente sencillo: con AutoMapper convertimos la lista de clases de dominio en una lista de modelos, y sin más le indicamos a EF que grabe todos los datos del modelo en la correspondiente tabla, invocando los métodos AddRange(), y SaveChanges().

Grabando miles de registros con ADO.NET + Reflection + Datatable + Procedimiento Almacenado

Vamos a ver el segundo de los casos. Como indicábamos en la introducción de este post, esta técnica puede resultar algo más farragosa, ya solo el título es más farragoso, pero, ¿será más eficiente? Vamos a comprobarlo.

private TestCases ADOBulkData_Datatable(List<parte_horas> _ParteAnual, TestCases _test)
{
    TestCaseExecutions _testexec = new TestCaseExecutions() { idTestCase = _test.id };

    //registra inicio
    _testexec.dtBegin = DateTime.Now;
    InitTestCase(_test.Function, _testexec.dtBegin);

    //ejecuta testcase
    data_test_sql _data_test_sql = new data_test_sql(_df.GetLabsCnx());

    //1. inicia conversión
    DateTime dtInicioConversion = DateTime.Now;
    SetMsg("Inicia conversión con reflection => parte_horas class to DataTable");

    DataTable _dtParteHoras = functions.reflections.CreateDataTable<parte_horas>(_ParteAnual, new List<string>() { "TipoJornada" });

    //fin conversión
    DateTime dtFinConversion = DateTime.Now;
    TimeSpan _ts = dtFinConversion - dtInicioConversion;

    SetMsg("Conversión completada " + _ts.TotalMilliseconds.ToString() + " milisegundos");

    //2. inicia grabación
    _data_test_sql.InsertParteHorasAnual(_dtParteHoras);

    //registra fin
    _testexec.dtEnd = DateTime.Now;
    EndTestCase(_test.Function, _testexec);

    return _test;
}

En este caso, aparte del registro de inicio, antes de proceder con la grabación debemos realizar un paso intermedio, la conversión de la lista de clases de dominio, a un DataTable. ¿Por qué? Bueno, pues porque este DataTable es el tipo de objeto que admitirá como parámetro de entrada un procedimiento almacenado.

La técnica que se usa para descubrir las propiedades de una clase se llama Reflection. Con este conjunto de funciones vamos a crear un método genérico que admitiría cualquier tipo de clase, y en base a su definición devolvería un DataTable cuyas columnas tendrán el mismo nombre que las propiedades de la clase.

Como ven hemos añadido en el testcase una medida temporal para saber exactamente cuanto tiempo se toma esta operación.

public static DataTable CreateDataTable<T>(IEnumerable<T> list, List<string> ExcludedProp)
{
    Type type = typeof(T);
    var properties = type.GetProperties();

    DataTable dataTable = new DataTable();

    int numElems = 0;

    foreach (PropertyInfo info in properties)
    {
        if (!ExcludedProp.Exists(pn => pn == info.Name))
        {
            dataTable.Columns.Add(new DataColumn(info.Name, Nullable.GetUnderlyingType(info.PropertyType) ?? info.PropertyType));
            numElems++;
        }
    }

    foreach (T entity in list)
    {
        object[] values = new object[numElems];

        for (int i = 0; i < numElems; i++)
        {
            values[i] = properties[i].GetValue(entity);
        }

        dataTable.Rows.Add(values);
    }

    return dataTable;
}

El método GetProperties descubre las propiedades de la clase y las recorre, para crear la definición de columnas del DataTable resultante.

Una vez creadas las columnas, en el segundo bucle transferimos los datos de la clase, al DataTable.

Observen que la propiedad TipoJornada, que está en la clase de dominio, pero no en la base de datos, me creaba algunas complicaciones, por lo que decidí añadir el parámetro ExcludeProp que sirve para ignorar las propiedades que ahí se informen.

Volviendo al testcase, cuando ya hemos obtenido el DataTable _dtParteHoras, llamamos al método InsertParteHorasAnual, en su versión para ADO.NET, que recibe como parámetro de entrada el mencionado y recién obtenido DataTable.

public bool InsertParteHorasAnual(DataTable _tblParteAnual)
{
    try
    {
        SqlConnection cnx = new SqlConnection(str_cnx);
        SqlCommand cmd = new SqlCommand();

        cmd.Connection = cnx;
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.CommandText = "test.insertParteHoras";

        cmd.Parameters.AddWithValue("@ParteHoras", _tblParteAnual);

        cnx.Open();
        cmd.ExecuteNonQuery();
        cnx.Close();
    }
    catch (Exception ex)
    {
        return false;
    }

    return true;
}

No hace más que llamar al procedimiento almacenado test.InsertParteHoras, pasándole el DataTable como parámetro de entrada. ¡Ojo! Para poder realizar esta operación, en la base de datos debemos crear, además del procedimiento almacenado que ahora veremos, una definición para este parámetro especial de tipo DataTable, a través de un tipo definido por el usuario de tipo tabla. Veamos cómo.


En esta captura les estoy mostrando la estructura de nuestra maravillosa base de datos. Se puede apreciar donde encontramos los tipos definidos por el usuario, que se definen de forma parecida a una tabla normal. Puesto que el procedimiento almacenado contiene un parámetro del tipo test.tblParteHoras, será necesario primero crear el tipo personalizado, y después el procedimiento almacenado. Por esta misma lógica el sistema no nos permitirá borrar este tipo definido por el usuario, sin eliminar antes todas sus dependencias, es decir, el o los procedimientos almacenados que lo usen.

Para crear nuestro User Defined Table Type es tan sencillo como ejecutar este script:

CREATE TYPE [test].[tblParteHoras] AS TABLE(
 [Trabajador] [uniqueidentifier] NOT NULL,
 [Fecha] [datetime] NOT NULL,
 [Horas] [smallint] NOT NULL
)
GO

Y a continuación estaremos en disposición de crear nuestro procedimiento almacenado test.InsertParteHoras que utilice este tipo como parámetro de entrada:

ALTER procedure [test].[insertParteHoras]
@ParteHoras test.tblParteHoras readonly
as
begin
 insert into [test].[ParteHoras]
 ([Trabajador], [Fecha], [Horas])
 select 
 [Trabajador], [Fecha], [Horas]
 from
 @ParteHoras
end

¡Voilá! TestCase desarrollado. Ya solo nos queda darle al PLAY e interpretar los resultados. Hagan sus apuestas señores:

Bulk Data - Store Procedure vs Entity Framework - Resultados



Conclusiones

En la captura les muestro el resultado de dos ejecuciones, con resultados muy parecidos. EF ha resuelto la operación en 10-11 segundos, mientras que el procedimiento almacenado, incluyendo la conversión a DataTable ha tomado poco más de 100 milisegundos, de los cuales entre 40 y 60 corresponden a la conversión y movimiento de datos de la clase al DataTable.

Parece claro que la diferencia en cuestión de eficiencia es significativa; no obstante debemos retomar las consideraciones que hicimos al principio del post, y tomar con cautela estos datos, ya que la adopción o no de un ORM en nuestros desarrollos debe incluir otras consideraciones.

Tengan también en cuenta, queridos amigos, que existen fórmulas mixtas, consistentes en utilizar ORM para las operaciones más comunes y sencillas, y realizar excepciones de uso en aquellos casos más particulares o exigentes.

En próximas publicaciones, y tomando como punto de partida los datos que acabamos de guardar, mediremos la eficiencia de EF vs ADO a la hora de acceder a dichas informaciones. Como siempre, espero que les resultase interesante el post, y que comenten, se bajen el proyecto y participen activamente cuanto quieran.

Decirles que estoy preparando también un post de alto interés arquitectónico sobre principios SOLID y Test unitarios.

Les deseo salud, y que todo compile bien en sus vidas.

Hasta muy pronto.






Comentarios

Entradas populares de este blog

Proyecto GEOW. Implementando el patrón CQRS. 2ª parte

En el anterior post ( Proyecto GEOW. Implementando el patrón CQRS ) nos adentramos en el funcionamiento del proyecto GEOW que nos va a servir de base para implementar un patrón arquitectónico, CQRS , pensado para dar respuesta a sistemas con alta exigencia de lecturas y escrituras simultáneas. Para ello hemos creado una interfaz gráfica con una serie de figuras geométricas en movimiento. Ahora vamos a ir a la parte de EL DATO. Cada vez que uno de estos cuadrados cambia de posición envía una trama con sus propias características, y sus nuevas coordenadas. Cada uno de los cuadrados realiza un movimiento cada 300 milisegundos, y he llegado a probar con hasta unas 700 figuras. En estos niveles el software empieza a sufrir, pero más la parte gráfica. Aparentemente el sistema de grabación de coordenadas se mantiene en buena forma. Vamos a ver, precisamente, este sistema de grabación: Grabando lotes de coordenadas en BBDD En el objeto PointObj que representa cada una de las figuras, en el

Primeros pasos en Enigma Software Labs

En pocas palabras: vamos a probar y estresar distintas soluciones de software para un mismo problema, en busca de realidades matemáticas. Para ello, lo primero que necesitamos en averiguar el modo de publicar código aquí en Blogger . Para ello voy a usar un sistema súper sencillo y eficiente. Ahí va un ejemplo de código C# private void frmMonitor_Load ( object sender, EventArgs e) { TreeNode _tr = new TreeNode( "SQL Server Tips" ); TreeNode _tr2 = new TreeNode( "C# Code" ); treeCatalogo.Nodes.Add(_tr); treeCatalogo.Nodes.Add(_tr2); //splitContainer.Panel1.Focus(); //lstMonitor.Focus(); } Hilarante ¿verdad?   hilite.me  lo hace posible. Bien, paso 2, fácil y sencillo: vamos a crear una plataforma de testeo para C# en una bonita solución que compartiré en el repositorio GIT, para disfrute de todos. Enigma Software - ZM LABS Como posteriormente me dive