giovedì 11 febbraio 2010

Nhibernate custom type mapping e dati geospaziali

Ormai è da un pò di tempo che utilizzo Nhibernate nei progetti che seguo e ogni tanto capita di dover affrontare nuove esigenze.
Riporto qui il caso in cui si debba avere a che fare con il mapping di un classe che possiede proprietà di un tipo che non rientra tra quelli gestiti direttamente da Nhibernate.

A titolo di esempio riporto il caso in cui una classe abbia una proprietà di tipo SqlGeography. La classe SqlGeography è contenuta nell'assembly Microsoft.SqlServer.Types ed è la classe che rappresenta il data type Geography di SQL Server 2008 non direttamente supportato da NHibernate.
Il data type Geography è la rappresentazione binaria delle coordinate geografiche di un punto sulla terra (coppia latitudine longitudine).
Quello che vogliamo ottenere è il mapping della proprietà di tipo SqlGeography e il campo sul database di tipo Geography (rappresentazione binaria del testo "POINT (latitudine longitudine)")

Per ottenere ciò è necessario innanzitutto creare una nuova classe che abbia lo scopo di definire il modo in cui NH debba eseguire il mapping sul database. Tale classe deve ereditare dall'interfaccia IUserType (namespace NHibernate.UserTypes) e implementare i seguenti metodi:

public class SqlGeographyUserType : IUserType
    {
        public object DeepCopy(object value)
        {
            if (value == null)
                return null;
            var sourceTarget = (SqlGeography)value;
            SqlGeography targetGeography = SqlGeography.Point(sourceTarget.Lat.Value, sourceTarget.Long.Value,
                                                              sourceTarget.STSrid.Value);
            return targetGeography;
        }
       
        public object Assemble(object cached, object owner)
        {
            return DeepCopy(cached);
        }
       public object Disassemble(object value)
        {
            return DeepCopy(value);
        }

Assemble e Disassemble vengono chiamati da NH rispettivamente durante la lettura e la scrittura nella cache di secondo livello, dunque si devono preoccupare di restituire o creare l'oggetto nella sua versione in cache. In questo caso l'oggetto in cache sarà identico a quello restituito utilizzando il metodo DeepCopy.

        public bool Equals(object x, object y)
        {
            if (ReferenceEquals(x, y))
                return true;
            if (x == null || y == null)
                return false;
            return x.Equals(y);
        }

Il metodo Equals è utilizzato da NH per verificare se la proprietà è diversa da quella memorizzata nella snapshot salvata in memoria, così da provvedere al salvataggio su database.

        public int GetHashCode(object x)
        {
            return x.GetHashCode();
        }

        public bool IsMutable
        {
            get { return true; }
        }

IsMutable deve restituire true se la classe è Mutable. Per le classi non Mutable infatti (cioè quelle per cui NH non debba fare l'insert e l'update) NH ha alcune ottimizzazioni delle performance.

        public object NullSafeGet(IDataReader rs, string[] names, object owner)
        {
            object prop1 = NHibernateUtil.String.NullSafeGet(rs, names[0]);
            if (prop1 == null)
                return null;
            SqlGeography geo = SqlGeography.Parse(new SqlString(prop1.ToString()));

            return geo;
        }

NullSafeGet viene chiamato da NH per ottenere l'istanza della proprietà mappata (nel nostro caso l'oggetto di tipo SqlGeograhy) a partire dal datareader utilizzato per leggere dal database. Dalla lettura della stringa del tipo "POINT (lat lon)" otteniamo un istanza del tipo SqlGeography.

        public void NullSafeSet(IDbCommand cmd, object value, int index)
        {
            if (value == null)
                ((IDataParameter) cmd.Parameters[index]).Value = DBNull.Value;
            else
                ((IDataParameter) cmd.Parameters[index]).Value = ((SqlGeography) value).STAsText().Value;
            ;
        }

NullSafeSet
viene utilizzato invece per impostare i parametri da passare al Command quando NH esegue un'insert o un'update della proprietà sul database.
In questo caso passiamo a SQL la rappresentazione in stringa dell'oggetto di tipo SqlGeography ("POINT (lat lon)"). NH provvederà dunque ad eseguire una insert nel formato: INSERT INTO Table1 (id,Localization) VALUES (2,'POINT(lat lon)')

        public object Replace(object original, object target, object owner)
        {
            return DeepCopy(original);
        }

        public Type ReturnedType
        {
            get { return typeof (SqlGeography); }
        }

        public SqlType[] SqlTypes
        {
            get { return new[] {NHibernateUtil.String.SqlType}; }
        }
}

La proprietà SqlTypes è utile per indicare a NH quale tipo deve utilizzare nella creazione delle istruzioni DDL per la generazione dello schema.

Ora che la classe è pronta possiamo definirla nel file di mapping:

<property name="location" column="GeoLocation" type="MyNamespace.SqlGeographyUserType, MyNamespace.NhibernateMappings" />

Attenzione, nel caso vogliate che il vostro IUserType debba mappare più di una proprietà, e vogliate utilizzare quest'ultime nelle query HQL (ad esempio nel caso volessimo mappare singolarmente lat e lon e utilizzare nelle query HQL una sintassi del tipo localization.lat=...) allora è necessario che il vostro SqlGeographyUserType non erediti da IUserType ma da ICompositeUserType che aggiunge qualche metodo in più all'interfaccia IUserType.

Nessun commento:

Posta un commento