Disclaimer: This is a personal web page. Contents written here do not represent the position of my employer.

Sunday, December 03, 2006

 

Serialización binaria con NHibernate

He empezado a usar NHibernate en varios proyectos y estoy bastante impresionado por su potencia y su fiabilidad. Es un framework de mapeado objeto/relacional para .NET, bastante popular entre los de su gremio. Aunque su última versión es una beta, en mi opinión es bastante estable porque de momento no ha dado ningún tipo de problema. Podíamos haber usado la versión 1.0 estable, pero lo bueno de la versión 1.2 (beta2) es que ya tiene soporte de tipos genéricos y nulables (están migrando completamente a la API 2.0).

Sin embargo, en ciertos escenarios esta utilidad tiene que mejorar:

- Portabilidad: Al usar sólamente lenguaje administrado (cosa de la que me he cerciorado usando MoMA), en teoría debería ser multiplataforma y funcionar con Mono. Sin embargo, en una prueba rápida que he hecho, Mono ha lanzado una excepción, y parece que el bug está en alguna invocación que en concreto usa la librería DynamicProxy (parte del proyecto CastleProject), la cual es usada por NHibernate. Espero poder hacer pronto un bug report. (Recordemos, no obstante, que Mono aún no tiene soporte oficial completo de la versión 2.0 de las librerías de .NET.) [Actualización (1): ya notificado.]

- Soporte de bases de datos. Aunque soporta bastantes motores de base de datos, echo en falta la idea de poder usar DB4O por debajo, lo que le daría bastante rendimiento y facilidad de uso (ya que ni siquiera harían falta los archivos de mapeado en caso de que sólo usasemos bases de datos orientadas a objetos). Es una idea que me surgió y pregunté en alguna ocasión, pero que además mencioné en el blog de un empleado de db4objects, al que le gustó tal posibilidad también. [Actualización (4): También se ha debatido ligeramente el tema en el blog de Ayende Rahien, un crack del equipo de NHibernate.]

- Acotación de queries. Y es que me he encontrado con un escenario extraño en el que NHibernate se ha traído bastante más información de la BDD de la que debería, ya que el programador sólo estaba consultando la propiedad COUNT de una colección. Espero poner pronto también un bug report sobre esto en el JIRA de NHibernate (un programa de gestión de incidencias al estilo Bugzilla). [Actualización (2): reportado.]

- Parseo de los archivos de mappings. También he encontrado casos extraños en los que, por culpa de un error en los archivos de mapeo, NHibernate escupía una excepción de ADO.NET, en lugar de ser más escrupuloso en el parseo de estos archivos de mapeado. Esto ya lo he reportado.

[Elemento añadido en Actualización (6)]
- Interpretación errónea de algunas query's: acabo de mandar este correo a la lista de correo de NHibernate para que me digan si es un bug o no, y por tanto, si tendría que darlo de alta en su JIRA:

Hello.

I have a simple entity like this:

<?xml version="1.0" encoding="utf-8" ?<
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
<class name="Agent" table="Agents">

<id name="Id" column="IdAgent" unsaved-value="0">
<generator class="native" />
</id>
<property name="Login" column="LOGIN" length="20" not-null="false"/>
<many-to-one name="Client" column="IdClient" not-null="false" />
</class>
</hibernate-mapping>

If I create a wrong HQL query like this:

IList aTerminalGroup =
oSession.Session.CreateQuery(
"from Agent WHERE IdClient = " + 1
).List();

Shouldn't NHibernate complain about it because IdClient is a column and not a property? I guess the correct way of writing the query is using "from Agent g WHERE g.Client.Id = " + 1, don't you think? Is this a bug? Should I create a bug in JIRA?

[Elemento añadido en Actualización (7)]
Al final lo he reportado.

- Por último, y básicamente la razón de esta entrada: la serialización de objetos procedentes de las clases de las que hace uso NHibernate, que a continuación paso a explicar en detalle.

En un escenario en el que el propio desarrollo que queremos llevar a cabo es cliente/servidor, ya sea usando Remoting o Servicios Web, se llega a la situación en la que se requiere enviar información sobre los objetos de nuestro dominio, y ¿qué mejor técnica que el envío completo del objeto a la parte interesada en él? Hay gente detractora de esta técnica pero renegar de ella supone duplicar esfuerzos en la codificación para establecer un sistema de paso de mensajes que al final es una copia de las estructuras de clases de nuestro dominio. Por tanto, si optamos por el camino rápido y productivo, se requerirá la serialización de los objetos de nuestro dominio (los que recojemos y tratamos, para después manipularlos mediante NHibernate para persistirlos) para poder transportarlos de un lado a otro, operación ésta que conlleva bastantes problemas pues:

1) Si optamos por una serialización de tipo XML, nos encontraremos finalmente con que es imposible llevarla a cabo, en la mayoría de los casos, con el serializador XML predeterminado de .NET, ya que las referencias recursivas no las trata y por tanto devuelve excepciones al encontrarlas (y tener referencias recursivas en los objetos del dominio es muy común ya que los objetos hijos normalmente tienen referencia también al objeto padre). Quién sabe, quizás un día me dé por escribir un serializador XML que soporte recursividad, pero mientras tanto hay que optar por la serialización binaria ([Actualización (3): debate en la lista de mono sobre serialización XML de objetos cíclicos, la cual al parecer se soluciona con Indigo/WCF] [Actualización (6): El SoapFormatter serializa sin problemas objetos cíclicos en XML, pero ahora mi problema es que no sé cómo hacer para que un WebService use este formatter por defecto, sospecho que la solución es usar Remoting :( ]). Sin embargo siguen existiendo dos problemas adicionales: los proxies y las colecciones lazy, que paso a explicar en los siguientes puntos.

Antes de nada, una pequeña introducción: NHibernate implementa desde varias versiones atrás lo denominado "lazy queries", o bien en español (aunque el término queda bastante peor): "consultas vagas". Sin ellas, NHibernate al recoger un objeto de la BDD y empezar a formarlo, se encontraría que tendría que recorrer bastantes tablas para rellenar los campos procedentes de las claves ajenas. Si esos campos son otras entidades que se encuentran en otras tablas, las cuales a su vez tienen otras claves ajenas, podemos provocar una consulta enorme que, o bien tarde bastante tiempo, o bien se traiga toda la base de datos con una consulta (dependiendo de lo cohesionadas que estén las entidades del dominio entre sí). Por tanto, el resultado de una "consulta vaga" serán entidades vagas o entidades con hijos o colecciones vagos: lazy collections. Las colecciones vagas permiten a NHibernate traer en una consulta sólo los atributos de una tabla, y dejar sus relaciones en modo suspendido de tal forma que si se accede a ellas, es cuando NHibernate solicitará una nueva consulta. Digamos que es una introspección de datos bajo demanda. Por tanto, este tipo de colecciones, si bien no era obligatorio utilizarlas, cada vez uno se da más cuenta de que es completamente necesario, y es por esto por lo que en la versión 1.2 de NHibernate vienen activadas por defecto.

Ahora que todos sabemos lo que son las lazy collections, puedo explicar el problema que tiene la serialización con ellas. El primero es que NHibernate, para poder implementarlas en los objetos de nuestro dominio, monta sobre nuestros interfaces IList/IDictionary unas colecciones internas, de un tipo implementado en las librerías de NHibernate, que contienen una referencia a la sesión abierta en el momento del acceso (por ejemplo el tipo NHibernatePersistenGenericBag). Además, los objetos de los que sólo tiene información sobre su identificador, por ser fruto de una clave ajena, los inserta una especie de proxy que sirve para que sean refrescados cuando se acceda alguna de sus propiedades, para que NHibernate pueda hacer una consulta SQL extra a la tabla de la que procede la clave ajena y rellenar los campos que faltan. Y ahora vamos con los inconvenientes:

2) Una colección IList/IDictionary que por dentro implementa un tipo de NHibernate no podrá ser deserializada por una aplicación que no tenga acceso a las librerías de NHibernate. ¿Acaso esto es un problema? Pues relativamente sí, porque si la parte que deserializa es un cliente de un WebService, ¿por qué iba a requerir las librerías de NHibernate si él no va a tener acceso directo a la base de datos? Además, estas librerías ocupan 1MB más o menos, lo que complica el despliegue de la aplicación (sobre todo en un entorno ClickOnce como en el que me encuentro).

3) Una clase que tiene insertado un proxy no se puede serializar.

Estos dos problemas tienen solución, pero no es trivial. La solución al problema del proxy es usar una rutina interna de NHibernate denominada "Unproxy", de la cual tuve conocimiento al preguntar en las listas de desarrollo de NHibernate y CastleProject (pues recordemos que NHibernate usa una librería de éste último proyecto). El problema de esta rutina es que sólo le quita el proxy a un objeto de nuestro dominio, pero no a sus hijos, por lo que seguimos teniendo otro problema derivado.

La solución al problema de las colecciones es transformarlas en colecciones nativas de .NET (List y Dictionary) en tiempo de ejecución, lo cual se puede hacer no sin antes encontrarse con el problema de que las colecciones aún no recuperadas no podrán convertirse en colecciones normales, por lo tanto necesitaremos un modo de marcar los objetos de nuestro dominio que estén "incompletos".

La conclusión a todo esta maremagnum de problemas y soluciones no es otra que un método que he desarrollado que podríamos bautizar como un "Unproxy recursivo", y que, mediante Reflection, recorre el objeto que queramos serializar para quitarle proxies, convertir colecciones NHibernate en nativas, y marcar objetos incompletos. Voy a exponer el código aquí y lo voy a publicar con licencia LGPL con el ánimo de que así contribuya más gente a mejorarla.

El código dista mucho de ser óptimo y estable, seguro que alguien encuentra pegas con ello. Pero seguro que entre varios podemos mejorarlo y debatir sobre él. A pesar de que es muy reciente, he comprobado su eficiencia y es bastante rápido (además he hecho algunas optimizaciones al acceso dinámico a los métodos, que se pueden notar al ver el uso de la clase RuntimeMethodHandle):


//Copyright 2006. Andres G. Aragoneses
//Code licensed under the LGPL: http://www.gnu.org/licenses/lgpl.html
using System;
using System.Collections.Generic;
using System.Text;

using System.Reflection;

using NHibernate;
using NHibernate.Proxy;

using MyApp.DataModel.DomainModel;
using MyApp.DataModel.Exceptions;

namespace MyApp.DataModel
{
public static class DataLayer
{
private static ISession oSession = null;

private static ISessionFactory oFactory = null;

public static ISession GetNewSession()
{
if (DataLayer.oSession == null)
{
//handlers initialization
fHandlerCreateGenericList = GetMethodHandle("CreateGenericList");
fHandlerCreateGenericDictionary = GetMethodHandle("CreateGenericDictionary");
fHandlerUnproxyList = GetMethodHandle("UnproxyList");
fHandlerUnproxyDictionary = GetMethodHandle("UnproxyDictionary");
fHandlerUnproxyCompletely = GetMethodHandle("UnproxyCompletely");


//NH initialization
NHibernate.Cfg.Configuration oCfg = new NHibernate.Cfg.Configuration();
oCfg.AddAssembly("DataLayer");
oCfg.AddAssembly("DomainModel");
DataLayer.oFactory = oCfg.BuildSessionFactory();
DataLayer.oSession = oFactory.OpenSession();
}
else
{
DataLayer.oSession.Close();
DataLayer.oSession = DataLayer.oFactory.OpenSession();
}
return DataLayer.oSession;
}

private static Dictionary<Type, Dictionary<int, DomainObject>> hUnproxyingOrAlreadyUnproxiedObjects = null;

//these structures are for optimizing reflection (see http://msdn.microsoft.com/msdnmag/issues/05/07/Reflection/default.aspx)
private static RuntimeMethodHandle fHandlerCreateGenericList;
private static RuntimeMethodHandle fHandlerCreateGenericDictionary;
private static RuntimeMethodHandle fHandlerUnproxyList;
private static RuntimeMethodHandle fHandlerUnproxyDictionary;
private static RuntimeMethodHandle fHandlerUnproxyCompletely;

private static RuntimeMethodHandle GetMethodHandle(string sName){
return typeof(DataLayer).GetMethod(sName,
BindingFlags.Static | BindingFlags.NonPublic).MethodHandle;
}

public static T Unproxy<T>(DomainObject maybeProxy, ISession oSession) where T : DomainObject
{
hUnproxyingOrAlreadyUnproxiedObjects = new Dictionary<Type, Dictionary<int, DomainObject>>();
oSession.Close();
return UnproxyCompletely<T>(maybeProxy);
}

private static bool IsProxy(DomainObject oProxy)
{
return (oProxy is INHibernateProxy);
}

private static T UnproxyCompletely<T>(DomainObject oProxy) where T : DomainObject
{
T oNoProxyBaseButMaybeProxyChildren = null;

if ((hUnproxyingOrAlreadyUnproxiedObjects.ContainsKey(typeof(T))
&& (hUnproxyingOrAlreadyUnproxiedObjects[typeof(T)].ContainsKey(oProxy.Id))))
{
return (T)hUnproxyingOrAlreadyUnproxiedObjects[typeof(T)][oProxy.Id];
}

//DELETEME:
//Console.WriteLine(typeof(T).ToString() + oProxy.Id);

if (DataLayer.IsProxy(oProxy))
{
//throw new InvalidOperationException("Cannot unproxy a not proxied object");
LazyInitializer oLazyInitializer =
NHibernateProxyHelper.GetLazyInitializer((INHibernateProxy)oProxy);
try
{
oNoProxyBaseButMaybeProxyChildren = (T)oLazyInitializer.GetImplementation(); //unwrap the object
}
catch (NHibernate.LazyInitializationException)
{
T oNewNeedLoadObject = Activator.CreateInstance<T>();
oNewNeedLoadObject.Id = oProxy.Id;
oNewNeedLoadObject.NeedLoad = true;

if (!hUnproxyingOrAlreadyUnproxiedObjects.ContainsKey(typeof(T)))
{
hUnproxyingOrAlreadyUnproxiedObjects[typeof(T)] = new Dictionary<int, DomainObject>();
}
hUnproxyingOrAlreadyUnproxiedObjects[typeof(T)][oProxy.Id] = oNewNeedLoadObject;
return oNewNeedLoadObject;
}
}
else
{
oNoProxyBaseButMaybeProxyChildren = (T)oProxy;
}

if (!hUnproxyingOrAlreadyUnproxiedObjects.ContainsKey(typeof(T)))
{
hUnproxyingOrAlreadyUnproxiedObjects[typeof(T)] = new Dictionary<int, DomainObject>();
}
hUnproxyingOrAlreadyUnproxiedObjects[typeof(T)][oProxy.Id] = oNoProxyBaseButMaybeProxyChildren;


//convert NHibernateGenericBag<> to List<>
foreach (PropertyInfo oProp in oNoProxyBaseButMaybeProxyChildren.GetType().GetProperties())
{
if ((!oProp.PropertyType.IsInterface) && (!oProp.PropertyType.IsClass))
{
//it is a value type, so skip
continue;
}

object oActualValue;
try
{
oActualValue = oProp.GetValue(oNoProxyBaseButMaybeProxyChildren, null);
}
catch (Exception oException)
{
if ((oException.InnerException != null) &&
(oException.InnerException is NHibernate.LazyInitializationException))
{
//set the list to null (equivalent to IList.NeedLoad == true)
oProp.SetValue(oNoProxyBaseButMaybeProxyChildren, null, null);
continue;
}
else if (oException.InnerException is IrmNeedLoadException)
{
//inform of another exception?
//(check mappings if you have a property in the class that is not in the mapping)
throw;
}
else
{
throw;
}
}

if (oActualValue == null)
{
//it is null, so skip
continue;
}

if ((oProp.PropertyType.IsGenericType))
{
MethodInfo fCreateMethod = null, fUnproxyMethod = null;

if (oProp.PropertyType.GetGenericTypeDefinition() == (typeof(IList<>)))
{
fCreateMethod =
(MethodInfo)MethodInfo.GetMethodFromHandle(fHandlerCreateGenericList);
fUnproxyMethod =
(MethodInfo)MethodInfo.GetMethodFromHandle(fHandlerUnproxyList);
}
else if (oProp.PropertyType.GetGenericTypeDefinition() == (typeof(IDictionary<,>)))
{
fCreateMethod =
(MethodInfo)MethodInfo.GetMethodFromHandle(fHandlerCreateGenericDictionary);
fUnproxyMethod =
(MethodInfo)MethodInfo.GetMethodFromHandle(fHandlerUnproxyDictionary);
}

fCreateMethod = fCreateMethod.MakeGenericMethod(oProp.PropertyType.GetGenericArguments());
fUnproxyMethod = fUnproxyMethod.MakeGenericMethod(oProp.PropertyType.GetGenericArguments());

if (fCreateMethod != null)
{
//NOT NEEDED: as we are doing it in the catch
//oProp.SetValue(oNoProxyBaseButMaybeProxyChildren, null, null);

try
{
object oChangedValue =
fCreateMethod.Invoke(null,
new object[] {
oActualValue
});
oProp.SetValue(
oNoProxyBaseButMaybeProxyChildren,
oChangedValue,
null);

oActualValue = oChangedValue;

try
{
fUnproxyMethod.Invoke(null, new object[] { oActualValue });
}
catch (Exception)//oException)
{
throw;
}
}
catch (Exception oException)
{
if ((oException.InnerException != null) &&
(oException.InnerException is LazyInitializationException))
{
oProp.SetValue(
oNoProxyBaseButMaybeProxyChildren, null, null);
//oNoProxyBaseButMaybeProxyChildren.NeedLoad = true;
}
else
{
throw;
}
}

}
else
{
throw new NotSupportedException("Generic type not supported yet");
}
}
else if (oActualValue is INHibernateProxy)
{
try
{
MethodInfo fMethod =
(MethodInfo)MethodInfo.GetMethodFromHandle(fHandlerUnproxyCompletely);
fMethod = fMethod.MakeGenericMethod(oProp.PropertyType);

oProp.SetValue(
oNoProxyBaseButMaybeProxyChildren,
fMethod.Invoke(null, new object[] { oActualValue }), null);
}
catch (Exception oException)
{
if (!(oException is LazyInitializationException))
{
throw;
}
else
{
T oNewManyToOneObject = Activator.CreateInstance<T>();
oNewManyToOneObject.Id = ((DomainObject)oActualValue).Id;
oNewManyToOneObject.NeedLoad = true;
//set a fallback object with NeedLoad as TRUE, in case of exception
oProp.SetValue(oNoProxyBaseButMaybeProxyChildren,
oNewManyToOneObject, null);
}
}
}
}

return oNoProxyBaseButMaybeProxyChildren;
}

private static List<T> CreateGenericList<T>(IList<T> aList) where T : DomainObject
{
List<T> aNewList = new List<T>(aList);
return aNewList;
}

private static Dictionary<K, V> CreateGenericDictionary<K, V>(IDictionary<K, V> hDict)
where K : DomainObject where V : DomainObject
{
Dictionary<K, V> hNewDict = new Dictionary<K,V>(hDict);
return hNewDict;
}

private static void UnproxyList<T>(List<T> aList) where T : DomainObject
{
for (int i = 0; i < aList.Count; i++)
{
aList[i] = UnproxyCompletely<T>(aList[i]);
}
}

private static void UnproxyDictionary<K,V>(Dictionary<K,V> hDict)
where K : DomainObject where V : DomainObject
{
IList<K> aKeysToRemove = new List<K>();
IDictionary<K, V> aKeysToAdd = new Dictionary<K, V>();
foreach (K oKey in hDict.Keys)
{
aKeysToAdd.Add(UnproxyCompletely<K>(oKey), UnproxyCompletely<V>(hDict[oKey]));
aKeysToRemove.Add(oKey);
}

foreach (K oKey in aKeysToAdd.Keys)
{
hDict.Add(oKey, aKeysToAdd[oKey]);
}

foreach (K oKey in aKeysToRemove)
{
hDict.Remove(oKey);
}
}
}
}


Para que la pieza de código funcione, hay que colocarla bajo una clase denominada DataLayer (o bien modificarlo para usar el nombre que queramos), y además tenemos que marcar nuestros objetos del dominio como clases derivadas de la siguiente superclase DomainObject (ídem):

namespace DataModel.DomainModel
{
[Serializable]
public abstract class DomainObject
{

public abstract int Id
{
get;
set;
}


protected bool bNeedLoad = false;

public virtual bool NeedLoad
{
get { return this.bNeedLoad; }
set { this.bNeedLoad = value; }
}

}
}


Es posible que haya mejores formas de solucionar estos problemas y, estaría encantado de oirlas. Pero mientras no sea así tendré que lidiar con este método que, evidentemente, ralentizará un poquito cada operación de serialización. Es posible que esto sea el nacimiento de algún pequeño proyecto que podría incluso donar al proyecto NHibernate como una especie de plugin. O también es posible que cuando quisiera ofrecerlo, sus programadores se den cuenta de la cosa tan horrible que he hecho, y que me informen de que se podría solucionar con métodos mejores y más eficientes simplemente modificando el core del proyecto NHibernate para proporcionarle ciertas funcionalidades extra en favor de la serialización. No lo sé, lo iremos viendo con el tiempo, pero mientras, ahí queda eso.

P.D.: Aprovecho la entrada para saludar a los lectores de PlanetaCodigo, los cuales en teoría serán capaces de leerme a partir de ahora cuando publique una entrada en mi categoría Programación.

Actualización 14-DIC-2006: Corregido comentario sobre licencia. Corregido bug en el método (comentada una línea). Añadido enlace a bug de Mono encontrado.

Actualización 09-ENE-2007: Añadido enlace al bug de acotación de queries.

Actualización 16-ENE-2007: Añadido enlace a debate sobre serialización XML de objetos cíclicos.

Actualización 29-ENE-2007: Añadido enlace a blog de Ayende Rahien. Interesante conversación en la lista de correo de NHibernate Hispano sobre el tema.

Actualización 17-ABR-2007: Vaya, parece que Ayende Rahien (uno de los desarrolladores de NHibernate) está en desacuerdo con la técnica de compartir un modelo de datos en todo el desarrollo. Tendré que refinar más mi idea y debatirlo abiertamente.

Actualización 20-JUL-2007: Añadido comentario sobre SoapFormatter y un nuevo inconveniente de NHibernate (¿bug?).

Actualización 20-AGO-2007: Al final el inconveniente que encontré en la última actualización de esta entrada lo he reportado como bug.

Actualización 28-SEP-2007: He encontrado otra limitación, que ya exploré en su día y no hay solución elegante posible de momento: HQL recursivo.

Actualización 08-DIC-2007: Parece que la nueva versión de NHibernate (2.0 en fase Alpha) tiene un nuevo parser de HQL que propiciará una mayor mantenibilidad para poder arreglar bugs y crear nuevas funcionalidades más fácilmente, como por ejemplo la que ya he mencionado en la anterior actualización de esta entrada: HQL recursivo.

Actualización 26-DIC-2007: El "Recursive HQL" ya está en JIRA.

Actualización 14-FEB-2008: En un artículo muy interesante de Ayende Rahien se habla de posibles workarounds a la implementación de jerarquías en BDD para evitar la limitación de NHibernate (y de las BDD en general, claro, pues no todas tienen estas capacidades de búsqueda recursiva y por tanto no todas soportarían la nueva funcionalidad que se desarrollare en NHibernate).

Actualización 15-ABR-2008: Al parecer se han corregido ciertos problemas en la rama trunk de NHibernate (la que sera la proxima version 2.0) que prevenian que la gente pudiera compilarlo con Mono: NH-1242, NH-1243, NH-1244 y NH-1245. Ahora es mas facil ser colaborador sin usar Microsoft :)

Labels: , , ,


Comments:
Yo me encuentro con el mismo problema. ¿Todavía creés que esa es la mejor forma de solucionar el problema? ¿Me recomendás que use tu solución?
Saludos, gracias
 
Yo había hecho un comentario y una consulta, puede ser que no haya llegado? vuelvo a hacer la consulta?
saludios, gracias
 
Sí, puedes probar a usarla, aunque últimamente no he usado NHibernate y no sé si seguirá funcionando con las nuevas versiones...
 
Post a Comment



<< Home

This page is powered by Blogger. Isn't yours?

Categories

RSS of the category Gnome
RSS of the category Mono
RSS of the category C#
RSS of the category Programming
RSS of the category Mozilla
RSS of the category Web Development
RSS of the category Security
RSS of the category Open Source
RSS of the category Engineering
RSS of the category Misc
RSS of the category Politics

Contact with me:
aaragonesNOSPAMes@gnNOSPAMome.org

Archive
My Photo
Name:
Location: Hong Kong, Hong Kong
Follow me on Twitter