NHibernate Avanzado: Proyecciones anidadas

Una de los argumentos más repetidos en contra de los ORMs es su falta de eficiencia, especialmente a la hora de hacer lecturas de grandes volúmenes de datos.

En realidad, esto suele estar provocado por un mal uso del ORM e intentar cargar muchas entidades en memoria para mostrar sólo un par de datos de cada entidad, pero hay una manera fácil de solucionar esto: usar proyecciones para cargar sólo los datos que necesitamos.

Todos los ORMs que conozco tienen algún sistema para construir este tipo de proyecciones, pero cuando los datos no son completamente «planos», la cosa se complica un poco. Con NHibernate podemos generar proyecciones anidadas, es decir, en las que un objeto tiene asociada una colección de valores de una manera relativamente sencilla usando IResultTransformer.

Como esto no es algo que haga todo los días y la documentación de NHibernate es como es (o sea, mala), en este post vamos a ver cómo podemos conseguirlo y seguro que mi yo del futuro lo agradece en algún momento.

Un modelo de ejemplo

Supongamos que tenemos un modelo simplificado para un blog en el que tenemos las entidades Post y Tag, cada una con varias propiedades, y con una relación muchos a muchos a entre ellas:

Modelo de Entidades

Sobre este modelo, queremos obtener un resumen en el que aparezca el título del post y los tags asociados, es decir, queremos cargar algo parecido a esto:

public class PostSummary
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string[] Tags { get; set; }
}

Para cargar los objetos PostSummary no podemos utilizar una proyección directa, ya que cada objeto contiene, además de información proyectada de la entidad Post, una colección (un array en este caso) con información proyectada de la entidad Tag.

Transfomers

Los Transformers de NHibernate nos permite transformar resultado de una consulta en objetos, generalmente «tontos», con la información de la consulta. NHibernate incluye de serie transformers para construir el objeto a partir de sus propiedades o a partir de su constructor. Ya hablamos un poco sobre ellos al comparar el rendimiento de NHibernate y Dapper para cargar datos.

Si necesitamos algo más complicado, como en este caso, podemos crear nuestro propio transformer, que es una clase que debe implementar el siguiente interfaz:

public interface IResultTransformer
{
    object TransformTuple(object[] tuple, string[] aliases);
    IList TransformList(IList collection);
}

El primer método, TransformTuple, se encargará de generar un objeto a partir de la información obtenida de la consulta. Los parámetros son los valores devueltos en cada «fila» de la consulta, y los alias asociados a cada valor. El segundo método, TransformList, nos permite manipular la colección generada al ejecutar el primer método sobre cada resultado de la consulta.

Puede parecer complicado, pero con un ejemplo veremos que no lo es tanto.

Para conseguir generar la proyección que queremos hacer, podríamos tener un QueryObject con esta pinta:

public class PostSummaryQuery : IQuery<IEnumerable<PostSummary>>
{
    public IEnumerable<PostSummary> Execute(ISession session) 
    {
        return session.CreateSQLQuery(@"
            select p.Id, p.Name, t.Name
            from Post p 
                 inner join PostTag pt on p.Id = pt.PostId 
                 inner join Tag t on pt.TagId = t.Id")
            .SetResultTransformer(new PostSummaryResultTransformer())
            .List<PostSummary>();
    }

    private class PostSummaryResultTransformer : IResutlTransformer
    {
        private class Row
        {
            public int PostId;
            public string PostTitle;
            public string TagName;
        }

        public object TransformTuple(object[] tuple, string[] aliases)
        {
            return new Row 
            {
                PostId = Convert.ToInt32(tuple[0]),
                PostTitle = Convert.ToString(tuple[1]),
                TagName = Convert.ToString(tuple[2])
            };
        }

        public IList TransformList(IList collection)
        {
            return collection.Cast<Row>()
                       .GroupBy(x => new {x.PostId, x.PostTitle})
                       .Select(x => new PostSummary
                       {
                           Id = x.Key.PostId,
                           Title = x.Key.PostTitle,
                           Tags = x.Select(y => y.TagName).ToArray()
                       }).ToArray();
        }
    }
}

Como véis, en el método TransformTuple estamos construyendo una estructura de datos auxiliar, el objeto Row, en que vamos procesando los resultados intermedios procedentes directamente de la consulta SQL, y en el método TransformList realizamos una agrupación en memoria para construir la estructura final de nuestra proyección con la colección de tags asociada a cada post.

Resumen

Los ORMs modernos incluyen muchas características que nos permiten obtener una buena eficiencia para la mayoría de las situaciones que podamos encontrarnos.

Si eres de los que creen que usar un ORM aporta muchas ventajas para desarrollar aplicaciones, es importante que conozcas este tipo de técnicas para minimizar el impacto en el rendimiento y conseguir un buen equilibrio entre la potencia del ORM y la velocidad de la aplicación.

Desgraciadamente, en el caso de NHibernate la documentación existente deja mucho que desear, estando muy repartida en distintos sitios y encontrándose en muchos casos desactualizada, por lo que no siempre es sencillo encontrar las mejores formas de resolver un problema.

4 comentarios en “NHibernate Avanzado: Proyecciones anidadas

  1. Hay dos aspectos que no me quedan claros:

    1. ¿Porqué se incluye el array «aliases» en el método «TransformTuple»?

    TransformTuple(object[] tuple, string[] aliases)

    2. En el método «TransformList(…)» supongo que se deberían utilizar los nombres de las columnas establecidas en la consulta SQL, sin embargo veo que no es así:

    * SQL:

    p.Id, p.Name, t.Name

    * TransformList(…):

    x.PostId, x.PostTitle, y.TagName

  2. Hola Lázaro,

    El array de alias se incluye por si desconoces (o puede variar) el orden de las columnas del resultado de la consulta SQL. La posición i-ésima del array aliases contiene el nombre dado al objeto i-ésimo del array tuple en la consulta.

    En este caso, en el que la consulta está escrita tan cerca del transformer puedes obviarlo, pero si vas a reutilizar el transformer con varias consultas viene bien.

    Lo que no sé es por qué no se pasa un IDictionary[string, object]. Supongo que será por herencia de Java o por motivos de rendimiento.

    En cuanto al TransformList, opera sobre una colección de objetos en memoria, donde cada objeto es uno de los devueltos por el método TransformTuple, de ahí que se usen directamente las propiedades de esos objetos en lugar de los alias de la consulta SQL.

    Espero haberte aclarado un poco las cosas.

    Un saludo.

  3. Me gusta mucho esta lógica para separar los query de todas las entidades de dominio, obtienes justamente lo que necesitas con un query que hace justamente lo que quieres :D El problema de esto es que terminas con N implementaciones de IResutlTransformer o al menos teniendo que especificar alias y proyecciones para cada Dto si usaras AliasToBean.

    Para ahorrarme un poco de trabajo lo que hago es crearme un transformer que me retorne dynamic result set http://adrianphinney.com/post/18900251364/nhibernate-raw-sql-and-dynamic-result-sets y luego por ser un poco estricto lo convierto a Dto usando https://github.com/randyburden/Slapper.AutoMapper

    Salu2

  4. Es una combinación muy buena, Omar. No conocía Slapper.AutoMapper, gracias por la aportación.

    La verdad es que esto de definir transformers a mano lo hago muy pocas veces, en general me apaño con el AliasToBean o el AliasToBeanConstructor.

    Los transformers manuales sólo los uso cuando necesito convertir cosas raras de la bd a objetos, o en casos como este en los que quiero construir una estructura de DTOs más compleja.

    Muchas gracias por la idea del dynamic+Slapper, me gusta.

Comentarios cerrados.