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:
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.