Carga de datos con NHibernate vs Dapper

En alguna ocasión he hablado de la forma que suelo usar para leer información de la base de datos y he mencionado que hoy en día existen varias alternativas, como el acceso directo con DataReaders, ORMs, MicroORMs, etc.

Los DataReaders son siempre la opción más eficiente pero también la más laboriosa, así que sólo los utilizaría como último recurso.

Tras ellos, los MicroORMs suelen ser bastante más rápidos que los ORMs, fundamentalmente porque hacen muchas menos cosas: no tienen identity map, no gestionan relaciones, no tienen lazy load, etc.

Sin embargo, cuando nos limitamos a cargar datos (no entidades) y usamos proyecciones con sentencias SQL optimizadas «a mano» para sacar el máximo rendimiento de la base de datos, estamos evitando gran parte de la carga que introduce un ORM y ahí es donde me surge la duda: si tienes un dominio complejo en el que compensa usar un ORM, ¿merece la pena introducir un MicroORM sólo para las lecturas?

Tenía ganas de hacer la prueba real y esta discusión en los comentarios del post de Pedro Hurtado me ha decidido a realizar la comparativa entre Dapper, un MicroORM diseñado para ofrecer el máximo rendimiento y usado en sitios como StackOverflow, y NHibernate, el que muchos consideramos como el ORM más completo y potente que puedes encontrar en .NET.

OJO: Las pruebas que he realizado no son muy científicas, más bien todo lo contrario. En ningún caso pretenden ser un benchmark completo y no tienen más valor del que le quieras dar.

El escenario de test

Para la prueba de rendimiento, he creado una tabla en SQL Server con esta estructura:

create table Person (
   id int identity(1,1) not null primary key,
   firstname nvarchar(50) not null,
   lastname nvarchar(50) not null,
   birthdate datetime not null,
   weight numeric(5,2) not null
)

La tabla es muy simple, pero contiene campos de tipos variados por si eso pudiera afectar de alguna forma a la prueba (que yo creo que no, pero bueno). En esa tabla he insertado 100K registros para tener unos cuantos datos.

La lectura de datos la he realizado a partir de la siguiente consulta:

select top N id, firstname, lastname, birthdate, weight from Person

La misma consulta la he lanzado para varios valores de N, cargando 1, 10, 100, 1000, y 10000 registros cada vez. Se trata de una consulta muy sencilla porque lo que quiero probar realmente es el rendimiento al construir objetos a partir de ella, no el rendimiento de la ejecución de la consulta, ya que éste depende de la base de datos y no del sistema que usemos para acceder (vale, esto no es del todo cierto, pero hagamos como si lo fuera…).

Para cargar los datos con Dapper he usado el siguiente código:

using (var connection = new SqlConnection(CONNECTION_STRING))
{
    connection.Open();
    var list = connection.Query<MyDTO>(string.Format(SQL_QUERY, rows)).ToList();
}

Con NHibernate, he usado dos alternativas:

  • AliasToBeanResultTransformer, que construye el objeto usando el constructor por defecto (sin parámetros) y luego asigna cada propiedad por separado.
  • AliasToBeanConstructorResultTransformer, que usa el constructor que le indiquemos para instanciar el objeto y pasarle todos los datos por el constructor, en lugar de ir propiedad a propiedad.

El código de cada una es:

// Con AliasToBeanResultTransformer
using (var session = factory.OpenSession())
{
    var list = session
        .CreateSQLQuery(string.Format(SQL_QUERY, records))
        .SetResultTransformer(Transformers.AliasToBean(typeof (MyDTO)))
        .List<MyDTO>();
}

// Con AliastToBeanConstructorResultTransformer
using (var session = factory.OpenSession())
{
    var list = session
        .CreateSQLQuery(string.Format(SQL_QUERY, records))
        .SetResultTransformer(Transformers.AliasToBeanConstructor(CONSTRUCTOR_INFO))
        .List<MyDTO>();
}

Los resultados

Los resultados los podéis ver esta tabla:

Comparativa Dapper vs NHibernate

En cada fila se muestra el tiempo de ejecución y el factor de penalización con respecto a Dapper, que como era de esperar es la opción más rápida.

Analizando un poco los resultados, tampoco hay sorpresas; está claro que lo que marca la eficiencia es la cantidad de Reflection utilizado:

  • Dapper usa LCG para generar dinámicamente métodos que mueven los datos del DataReader al DTO, evitando así el uso de reflection.
  • NHibernate con AliasToBeanConstructorResultTransformer usa reflection una vez por cada registro para invocar al constructor constructor.
  • NHibernate con AliasToBeanResultTransformer necesita usar reflection varias veces por cada registro: una para construir el objeto usando el constructor sin parámetros y otra para establecer cada propiedad.

Conclusiones

En la mayoría de los casos da igual lo que uses porque seguramente va a ser lo bastante rápido, a menos que tengas un número enorme de usuarios concurrentes o una limitación hardware muy importante.

Ten en cuenta que con estos datos, el sistema más lento tarda 15ms en leer 1000 DTOs de la base de datos, y leer 1000 cosas de lo que sea no debería ser una operación frecuente en ningún caso (¿qué vas a hacer con 1000 cosas? ¿pintarlas en un grid inmanejable?)

Seguramente si tienes problemas de rendimiento no sea por cosas como ésta, sino por otras aberraciones mucho más frecuentes como exceso de consultas a la base de datos, select n+1, etc.

Para mi usar Dapper u otro MicroORM en un proyecto en que ya estás usando NHibernate no tiene mucho sentido y prefiero ahorrarme tener una dependencia adicional (insisto, a menos que seas StackOverflow).

Eso no quita que haya situaciones en las que no tenga sentido es usar algo tan pesado como NHibernate pudiendo usar Dapper, y no me refiero al tema de la eficiencia, sino a todo lo que lleva asociado NHibernate (mappings, session factory, etc.). Además hay que reconocer que el API de Dapper es muy limpia y agradable.

Una última cosa, no soy un experto en Entity Framework, pero estoy casi seguro de que puedes cambiar NHibernate por EF en este post y seguiría teniendo sentido casi todo.

6 comentarios en “Carga de datos con NHibernate vs Dapper

  1. Pingback: Elige bien tus algoritmos

  2. Hi,
    I’ve spent some time looking for some NHibernate bean transformer performance benchmarks and I really appreciate your post. Especially because I am about to make decision whether to use NH + transformer or Dapper for reporting stored procedures results mapping.
    Keep in mind that Dapper is tool specialized in this kind of stuff and for NH it is just a sexy feature. According to NH documentation «The procedure must return a result set. NHibernate will use IDbCommand.ExecuteReader() to obtain the results.». I don’t know if it is valid for 3.* versions but it’s better to known that such limitation can occur.
    Best regards.

  3. Thanks, as I say in the post, Dapper is pretty good for that scenario and I probably would use it (or something similar like Simple.Data). The only reason I would still use NH in that case would be if it is already being used in the project.

    Regarding the result set limitation, I think it’s valid for NH3+, but I don’t see it as a limitation. What are you returning from your SPs?

  4. From my SPs I return reporting data mostly. I try to follow CQRS principles – use NHibernate for performing business logic and writes (where it’s most valuable to use objects) and mix it with some other technologies which better fits reading scenarios requirements. For reading purposes I often use Linq2SQL + DynamicLinq. I’m going to write on my blog about mixing NHibernate with other technologies (like Dapper) soon.

    Best regards.

  5. I agree, although I would use Dapper instead of Linq2SQL for the reads. Actually, I wrote about something similar in my series about repositories.

    By the way, good blog!

  6. Maybe reconsider linq2SQL + Dynamic Linq – it is able to create SQL queries on fly which enables you to do some useful stuff with grids for example (sorting, paging, grouping etc.)

    Thank you very much. I do my best to publish valuable stuff on my blog – maybe nothing very inventive but at least not another hello-worlds ;)

Comentarios cerrados.