Modificar la configuración de NHibernate por código

Por extraño que pueda parecer, y pese a las alternativas existentes (Fluent NHibernate, ConfORM, «Sexy Mapping» en NH3.2), todavía hay veces que nos encontramos con aplicaciones que usan XML para definir los mappings de las entidades.

Si tenemos la necesidad de modificar de forma global esos mappings, al estilo del uso de convenciones que existe en las librerías que mencionaba antes, hay dos opciones:

  • Tocar uno a uno todos los mappings para ajustarlos a lo que necesitamos.
  • Como buen programador, ser un vago y hacerlo por código.

Seguramente más por vago que otra cosa, la opción que más me atrae es la de poder cambiar la configuración por código y ahorrarme tocar un montón de mappings, así que jugando un poco con NHibernate, encontré una forma de hacerlo.

La clave está en las propiedades ClassMappings y CollectionMappings que expone el objeto Configuration de NHibernate. A través de estas propiedades podemos acceder a los mappings y modificarlos.

¿Que qué se puede hacer con esto? Pues veamos un par de ejemplos. Si queremos cambiar la estrategia de generación de POID en nuestras entidades, para que las que hemos definido como identity pasen a ser native, podemos usar algo parecido a esto:

foreach (var mapping in configuration.ClassMappings)
{
    var id = mapping.Identifier as SimpleValue;
    if (id == null || id.IdentifierGeneratorStrategy != "identity")
        continue;

    id.IdentifierGeneratorStrategy = "native";
    id.IdentifierGeneratorProperties.Add("sequence", "sq" + mapping.Table.Name);
}

En este caso es todo bastante sencillo, basta con recorrer los ClassMappings y modificar la estrategia de generación de ID. El modelo interno que usa NHibernate en su configuración, al menos a este nivel, es prácticamente idéntico al XML de los mappings, por lo que con un poco de ayuda del depurador es sencillo encontrar los puntos a tocar.

Otra posible utilidad es cambiar la forma en que se mapean ciertas propiedades, por ejemplo, para usar un IUserType en todas las propiedades de un cierto tipo (como el FixedPointUserType que definíamos para SQLite):

public static NHibernate.Cfg.Configuration AdaptToSQLite(this NHibernate.Cfg.Configuration configuration)
{
    configuration.BuildMappings();

    foreach (var mapping in configuration.ClassMappings)
        foreach (var property in mapping.PropertyIterator)
            AdaptProperty(property);

    foreach (var mapping in configuration.CollectionMappings)
    {
        var elementType = mapping.Element.Type;
        if (elementType.IsComponentType)
        {
            var subtypes = ((ComponentType)elementType).Subtypes;
            for (var i = 0; i < subtypes.Length; i++)
            {
                if (subtypes[i].GetType() == typeof(DecimalType))
                {
                    subtypes[i] = new CustomType(typeof(FixedPointDecimalUserType), new Dictionary<string, string>());
                }
            }
        }
    }

    return configuration;
}

public static void AdaptProperty(Property property)
{
    if (property.IsComposite)
    {
        var component = property.Value as Component;
        foreach(var childProperty in component.PropertyIterator)
            AdaptProperty(childProperty);
    }

    if (property.Type.GetType() == typeof(DecimalType))
    {
        var simpleValue = property.Value as SimpleValue;

        if (simpleValue != null)
            simpleValue.TypeName = typeof(FixedPointDecimalUserType).AssemblyQualifiedName;
    }
}

En este caso la cosa se complica un poco más porque es necesario tener en cuenta no sólo las clases (a las que accedemos a través de ClassMappings), sino también los elementos que aparecen en colecciones (Sets, Bags, Lists, etc., accesibles mediante CollectionMappings). Además, hace falta considerar los componentes y la forma en que estos se pueden anidar, por lo que tenemos que usar un poco de recursión para recorrer correctamente la configuración.

Con esto hemos visto un poco (muy poco) de cómo funciona la configuración de NHibernate y lo que podemos hacer con ella si es necesario. De todas formas, si no estamos tratando con un proyecto antiguo y tenemos la posibilidad de usar cualquiera de las formas de mapeo por código, es mucho mejor tirar por ese camino, ya que ofrece muchas posibilidad para aplicar todo este tipo de convenciones de forma sencilla y limpia.