JPA e Mapping Personalizzati

Grazie alla diffusione dei framework ORM (Object-Relational Mapping) la vita di uno sviluppatore che aveva intenzione di interfacciarsi con un database si è semplificata notevolmente, o per meglio dire è diventata più vivace! Vivace perché è più interessante capire come funziona un framework e come risolve i problemi per noi piuttosto che ripetere sempre la canonica struttura per impostare l’accesso ai dati: crea il DAO, gestisci l’apertura e la chiusura della connessione o della transazione, committa, gestisci eccezioni e rollback, crea un builder per costruire gli oggetti delle entità dal result set delle query, scrivi TUTTE le query… “accidenti abbiamo cambiato DB, quella query non funziona più!!” (e chi più ne ha più ne metta…).

E l’ordine fu…

Con l’arrivo delle specifiche EJB3 e JPA1, si è dato un po’ di ordine ai framework ORM esistenti (anche se, a mio avviso, Hibernate regnava incontrastato): adesso è possibile lavorare quasi indistintamente con prodotti come Hibernate, EclispeLink od OpenJPA, utilizzando annotazioni o XML. Dico quasi indistintamente perché la specifica JPA1 non copre tutti gli aspetti dell’ORM, per cui alcuni problemi rimangono demandati alle soluzioni proprietarie dei vendors. In particolare, la specifica JPA1 (e a quanto pare anche la 2) non propone una soluzione standard quando si ha la necessità mappare tra loro valori che, nel mondo ad oggetti ed nel mondo relazionale, sono rappresentati con tipi differenti o non direttamente comparabili.

Fuori specifica: i tipi personalizzati

Cosa si intende per tipo personalizzato? Facciamo un esempio. Ricordo che con Hibernate inizialmente non era possibile salvare un enum su database come stringa in modo semplice: andava implementato uno UserType per poter effettuare la conversione. Fortunatamente in JPA (e quindi adesso anche in Hibernate) è possibile mappare tranquillamente un enum tramite l’annotazione @Enumerated.
Per tipi personalizzati quindi possiamo intendere tutti quei casi in cui il mapping tra i tipi di dati del modello a oggetti e quello relazionale non coincidono o non sono compatibili, per cui è richiesta una sorta di conversione.

Mapping per valore

Come fare per esempio a mappare una variabile booleana di una classe su una colonna varchar(1) con valori 'S' o 'N'? Oppure come mappare un enumeratore {MASCHIO, FEMMINA} sui valori 'M' o 'F'? Il JPA non propone una soluzione standard al problema: proprio da qui nasce l’esigenza di avere dei tipi personalizzati (o convertiti) per valore. Vediamo per esempio come è possibile risolvere il problema in EclipseLink e in Hibernate.

EclipseLink Converter

Questa implementazione di JPA mette a disposizione una serie di convertitori che permettono di manipolare tipi e dati, configurabili tramite codice Java, annotazioni o XML. Ogni convertitore implementa l’interfaccia Converter: fortunatamente il framework fornisce già alcuni convertitori che permettono di risolvere i casi di conversione più comuni.
Per risolvere il problema del mapping dei booleani, per esempio, possiamo usare un ObjectTypeConverter: con esso è possibile mappare coppie di valori in modo da associare al valore di un oggetto (che chiameremo object value) il corrispettivo valore su database (data value).
Un convertitore può essere dichiarato tramite annotazioni (in una delle entità del nostro dominio per esempio). Ammettiamo di avere l’entità Account con un attributo booleano enabled

@Entity
public class Account implements Serializable {

     ...

     @ObjectTypeConverter (
          name="booleanConverter",
          dataType=java.lang.Boolean.class,
          objectType=java.lang.String.class,
          conversionValues={
               @ConversionValue(dataValue=Boolean.TRUE, objectValue="S"),
               @ConversionValue(dataValue=Boolean.FALSE, objectValue="N")}
     )
     @Convert("booleanConverter")
     private boolean enabled;

     ...

}
@ObjectTypeConverter
definisce il mapping tra i valori true/false e 'S'/'N'
@Convert
associa il convertitore alla variabile

Con EclipseLink quindi, riusciamo in un attimo a configurare un convertitore e ad associarlo alla variabile interessata con poche annotazioni.
Se volessimo usare il converter appena definito su un attributo booleano di un’altra entità, basterebbe annotarlo con @Convert("booleanConverter"), in quanto il tipo di convertitore è già stato definito dentro la classe Account.
Dichiarare un convertitore in una classe che poi verrà riusato anche in un’altra risulta poco manutenibile (per usare un eufemismo): in questi casi, a mio avviso, è consigliabile usare un file XML per definire il converter, in modo da centralizzarne le dichiarazioni. EclipseLink dispone di un file di configurazione (solitamente chiamato eclipselink-orm.xml), nel quale, tra le altre cose, possiamo dichiarare un convertitore come segue:


In questo modo abbiamo un file unico in cui verranno elencati tutti i convertitori del nostro dominio: per utilizzare un convertitore basta quindi annotare un attributo di una classe con @Convert("converterName").

Hibernate UserType

Con il passaggio a JPA, Hibernate ha mantenuto la propria gerarchia di UserType per personalizzare i tipi da utilizzare nel mapping ORM tramite le annotazioni TypeDef e Type. Diversamente da quanto visto in precedenza, in questo caso non è possibile definire un mapping object-data value in modo dichiarativo: ogni nuovo tipo dovrà implementare l’interfaccia UserType o una delle sue sottoclassi.
La classe dell’esempio precedente verrà annotata in modo differente:

@Entity
@TypeDef(typeClass = BooleanType.class)
public class Account implements Serializable {

     ...

     @Type(type = "booleanType")
     private boolean enabled;

     ...

}
@TypeDef
dichiara l’esistenza di uno UserType la cui classe è definita dall’attributo typeClass
@Type
associa il tipo alla variabile

Se intendiamo mappare in valore di un oggetto della classe K su un varchar, possiamo generalizzare gran parte del codice richiesto dall’interfaccia UserType, creando una classe parametrizzata chiamata StringBasedUserType:

public abstract class StringBasedUserType implements UserType {

     @Override
     public Object assemble(Serializable cached, Object owner)
                     throws HibernateException {
          return cached;
     }

     @Override
     public Object deepCopy(Object value) throws HibernateException {
          return value;
     }

     @Override
     public Serializable disassemble(Object value) throws HibernateException {
          return (Serializable) value;
     }

     @Override
     public boolean equals(Object x, Object y) throws HibernateException {
          return x.equals(y);
     }

     @Override
     public int hashCode(Object x) throws HibernateException {
          return x.hashCode();
     }

     @Override
     public boolean isMutable() {
          return false;
     }

     @Override
     public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
               throws HibernateException, SQLException {
          assert names.length == 1;
          Map conversionMap = getConversionMap();
          String dbValue = (String) StringType.INSTANCE.get(rs, names[0]);

          if (conversionMap.containsValue(dbValue) && !"".equals(dbValue)) {
               Set> entrySet = conversionMap.entrySet();
               for (Entry entry : entrySet) {
                    if (entry.getValue().equals(dbValue))
                         return entry.getKey();
                    }
               }
          throw new HibernateException("Cannot convert DB value " + dbValue);
     }

     @Override
     public void nullSafeSet(PreparedStatement rs, Object value, int index)
               throws HibernateException, SQLException {
          Map conversionMap = getConversionMap();

          if (conversionMap.containsKey(value)) {
               StringType.INSTANCE.set(rs, conversionMap.get(value), index);
          } else {
               throw new HibernateException("Cannot convert object value " + value);
          }
     }

     @Override
     public Object replace(Object original, Object target, Object owner)
               throws HibernateException {
          return original;
     }

     @Override
          public Class< ?> returnedClass() {
               return this.getClass().getTypeParameters()[0].getClass();
     }

     @Override
     public int[] sqlTypes() {
          return new int[] {StringType.INSTANCE.sqlType()};
     }

     public abstract Map getConversionMap();

}

Questa classe implementa tutti i metodi dell’interfaccia UserType e dichiara un metodo astratto nel quale verrà definita la mappa tra il valore di un oggetto e il valore su database.
I metodi più interessanti sono ovviamente nullSafeGet e nullSafeSet che si occupano rispettivamente di convertire i valori provenienti dal database in un oggetto e viceversa. Da notare che non vengono accettati valori nulli in conversione, di conseguenza la colonna interessata sul database è consigliabile che sia not nullable con un valore di default per evitare problemi in runtime.
La classe BooleanType è quindi semplicemente definita come:

public class BooleanType extends StringBasedUserType {

     @Override
     public Map getConversionMap() {
          Map map = new HashMap();
          map.put(true, "S");
          map.put(false, "N");

          return map;
     }
}

Per gli stessi motivi menzionati in precedenza, eviterei di usare @TypeDef su una entità se poi il tipo viene riusato in altre classi. Seguendo sempre il principio della centralizzazione, sfruttiamo la configurazione del framework tramite XML. Possiamo creare un file ad hoc, chiamato per esempio typedefs.hbm.xml, che raccoglierà la definizione di tutti i tipi personalizzati che intendiamo creare:

< ?xml version="1.0"?>
< !DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

    

Basta poi referenziarlo all’interno del canonico file di configurazione di Hibernate (hibernate.cfg.xml):

< ?xml version="1.0" encoding="UTF-8"?>
< !DOCTYPE hibernate-configuration PUBLIC
                "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
                "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

    
        
    

ed il gioco è fatto!
Ovviamente quello illustrato è un esempio banale che però può tornare utile in caso si abbia a che fare con un database legacy (in italiano!!). Dal canto suo Hibernate dispone già di convertitori booleani codificati sulla lingua inglese: @Type("yes_no") e @Type("true_false") rispettivamente scrivono su database 'Y'/'N' e 'T'/'F'.

Tipi personalizzati ed Enumeratori

Apparte la conversione dei booleani, a chi non è capitato di dover persistere il valore di un enumeratore? Come abbiamo già accennato, JPA permette facilmente questa operazione tramite l’annotazione @Enumerated. Ad esempio ammettiamo che la nostra entità Account disponga di un attributo type:

public class Account {
     
     ...

     @Enumerated
     private AccountType type;

     ...
        
}

del tipo:

public enum AccountType {

     BASIC, PRO, GOLD;

}

Come verrà persistito il suo valore? Secondo la specifica JPA:

  • se l’annotazione @Enumerated non ha attributi, di default il valore viene convertito nel numero corrispondente alla posizione in cui è dichiarato nell’enum: quindi 1 -> MALE e 2 -> FEMALE;
  • l’alternativa è specificare @Enumerated(EnumType.STRING): il valore verrà salvato come stringa rappresentata dall’istanza dell’enumeratore.

Se però abbiamo bisogno di salvare il valore dell’enumeratore in un modo diverso da quello predisposto dalla specifica JPA, entrano in gioco le soluzioni vendor mostrate in precedenza. Vediamo brevemente nel dettaglio come possono essere applicate.

Enum ed ElipseLink

In questo caso, usando l’ObjectTypeConverter, basta dichiarare:


nel file eclipselink-orm.xml in modo da mappare ogni valore dell’enum su una stringa con le sue iniziali.
A questo punto, annotare l’attributo della classe come segue:

     ...

     @Enumerated(EnumType.STRING)
     @Convert("accountTypeConverter")
     private AccountType type;

     ...

e il converter penserà al resto.

Enum e Hibernate

In questo caso la classe StringBasedUserType creata precedentemente può tornarci utile. Basta estenderla come segue:

public class AccountTypeUserType extends StringBasedUserType {

     @Override
     public Map getConversionMap() {
          Map map = new HashMap();
          map.put(AccountType.BASIC, "B");
          map.put(AccountType.PRO, "P");
          map.put(AccountType.GOLD, "G");

          return map;
     }
}

e inserirne la dichiarazione

   ...
  
   ...

nel file typedefs.hbm.xml creato in precedenza. Infine, annotare l’attributo della classe:

     ...

     @Enumerated(EnumType.STRING)
     @Type("accountTypeUserType")
     private AccountType type;

     ...

Conclusioni

Quando si ha a che fare con progetti che riguardano l’aggiornamento di applicazioni preesistenti, spesso il database non viene modificato per non incorrere in possibili problemi di perdite di dati, soprattutto in quei casi in cui vengono interessati dati sensibili. La progettazione di un nuovo applicativo sopra questi dati però non può legare e condizionare la modellazione, che ruota intorno a concetti e oggetti parlanti, ben chiari, che si autodocumentano: va da sé che lo stile degli acronimi che spesso si trova nei database legacy va contro questi principi. Fortunatamente, anche se la specifica non lo contempla, le implementazioni JPA sono talmente potenti da venire incontro alla maggior parte delle esigenze lavorative.
Abbiamo visto quindi come un problema semplice, come mappare una stringa su un database a partire da un booleano o un enumeratore, non sia così immediato. Dal confronto delle due soluzioni, a mio avviso EclipseLink permette di risolvere in modo molto elegante e sbrigativo la questione, senza scrivere praticamente una riga di codice tramite l’ObjectTypeConverter già “preconfezionato”. La soluzione proposta da Hibernate invece rimane più generica, fornendo tutti gli strumenti necessari agli sviluppatori per arrivare alla stessa soluzione del concorrente, richiedendo però un po’ più di sforzo.

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio . - -

  • Znfranco

    per la conversione in eclipselink della dat in formato dd/mm/yyyy a yyyy-mm-dd e viceversa, come posso fare ( cme db uso derby)?
    grazie

    • Ciao, per fare il mapping di una colonna data su db puoi utilizzare un campo java.util.Date (ricordati di mettere l’annotation @temporal per specificare se deve contenere solo la data o anche l’orario).
      Quando hai un oggetto Date lo puoi formattare come vuoi utilizzando la classe java SimpleDateFormat
      Fabio

    • Ciao, per fare il mapping di una colonna data su db puoi utilizzare un campo java.util.Date (ricordati di mettere l’annotation @temporal per specificare se deve contenere solo la data o anche l’orario).
      Quando hai un oggetto Date lo puoi formattare come vuoi utilizzando la classe java SimpleDateFormat
      Fabio