In due post precedenti (Eclipselink Historical Session: come tenere traccia dei cambiamenti di una tabella – Parte I e Parte II) è stato trattato il tema della gestione dello storico con EclipseLink: dalla configurazione, ai vari modi in cui può essere usato fino a presentare alcune limitazioni intrinseche al framework.

Tutto quel che si è detto in quei post è valido e funziona ammesso che abbiamo già creato lo schema su cui andremo a lavorare. Cosa succede invece se non lo abbiamo?

Andiamo per gradi: EclipseLink, come a suo tempo già Hibernate, permette di creare dinamicamente la struttura di uno schema in base alle annotazioni delle entità che gestisce, nonostante questo aspetto non sia coperto dalla specifica JPA.

Il framework infatti è capace di generare gli script SQL DDL (Data Definition Language) che servono a creare le tabelle e i vincoli che sussistono tra esse. In fase di configurazione di EclipseLink (quindi nel persistence.xml nel caso si lavori in Java EE) basta infatti popolare la proprietà eclipselink.ddl-generation con uno dei seguenti valori:

  • none: non viene generato nessuno script DDL;
  • create-tables: vengono generati ed eseguiti gli script CREATE TABLE per ogni entità. Se la tabella esiste già, viene eseguito il comportamento di default del database e del driver JDBC;
  • drop-and-create-tables: vengono generati ed eseguiti prima gli script di DROP e poi di CREATE TABLE per ogni entità.

A meno che non si abbia a che fare con un database legacy, è molto utile non doversi occupare della generazione delle tabelle.

Nel seguito, riprendiamo a titolo di esempio le entità usate nei due articoli menzionati all’inizio, aggiungendo qualche differenza. Ammettiamo che nel nostro dominio esista una relazione one-to-one tra l’entità UserData e User (che contiene per esempio solo i campi nome e cognome – che prima erano contenuti in UserData). Questa modifica ci permette di introdurre un problema che altrimenti non verrebbe affrontato, cioè quello delle foreign key sulle tabelle di storico.

Guai in vista

Cosa succede quando abbiamo bisogno della ddl-generation e della gestione dello storico insieme? A prima vista si direbbe: “e che c’entrano l’uno con l’altro?”. Ci si potrebbe aspettare che, se ho attivato lo storico sull’entità UserData, l’algoritmo che genera lo script DDL per la sua tabella corrispondente si preoccupi di generare anche quella relativa al suo storico.

Invece, udite udite, questo non avviene! Nel momento in cui provate a fare in primo inserimento dati sulla tabella user_data, EclipseLink si arrabbia notevolmente, dicendo che la tabella user_data_hist non esiste!! E adesso?? Dobbiamo creare le tabelle di storico manualmente? Addio quindi al sogno di un deploy automatico della nostra applicazione con un click? Per fortuna possiamo provare a girare intorno a questo problema e allo stesso tempo risolvere alcune limitazioni imposte dalla gestione dello storico del framework.

Gestire i dati storici come entità

Dal momento che sono solo le entità (quindi le classi annotate come @Entity) ad essere prese in considerazione durante la generazione degli script DDL, non ci resta che creare una nuova classe corrispondente alla tabella di storico. La nuova l’entità UserDataHist sarà:

@Entity
@Table(name="user_data_hist")
public class UserDataHist implements Serializable {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	private long id;

        @Id
	@Temporal(TemporalType.TIMESTAMP)
	@Column(name = "start_date")
	private Date startDate;

	@Temporal(TemporalType.TIMESTAMP)
	@Column(name = "end_date")
	private Date endDate;
	
        @OneToOne
        @JoinColumn(name="user_id", referencedColumnName="id")
	private User user;
	
	private String address;
	
	private String city;
	
	private String state;
	
	private int zip;
	
	@Column(name="phone_number")
	private String phoneNumber;
	
	@Column(name="e_mail")
	private String eMail;

...

}

L’entità originale UserData, come ci si può aspettare, sarà speculare a questa, ad eccezione dei due campi data inizio/data fine (di cui il primo è parte della chiave primaria come avevamo già spiegato). Già qua però qualcuno, giustamente, potrebbe storcere il naso: “ma questa è duplicazione di codice!”. Effettivamente neanche io dormirei la notte sonni tranquilli con una soluzione di questo tipo!

Oltre a questo c’è il problema dell’attributo user sull’entità di storico che genera una foreign key sulla tabella: la creazione di una foreign key su questa tabella è un fatto molto importante da evitare perché altrimenti si creano dei vincoli di integrità referenziale tra le tabelle di storico e le entità “normali”, portando a comportamenti alquanto bizzarri nei casi di update o delete.

Prima di risolvere la questione vediamo però come fare a dire ad EclipseLink che UserDataHist è l’entità di storico di UserData. Riprendiamo, sempre dal post precedente, la classe EntityHistorySessionCustomizer e modifichiamola come segue:

public abstract class EntityHistorySessionCustomizer implements SessionCustomizer {

     private static final String START_DATE = "start_date";
     private static final String END_DATE = "end_date";
     protected Map, Class> historyEntityMap = new HashMap, Class>();

     public EntityHistorySessionCustomizer() {
          buildHistoryEntityMap();
     }

     @SuppressWarnings("unchecked")
     public void customize(Session session) throws Exception {
          Set> historyEntitySet = historyEntityMap.keySet();

          for (Class clazz : historyEntitySet) {
               ClassDescriptor sourceDescriptor = session.getDescriptor(clazz);
               customize(sourceDescriptor, 
                    session.getDescriptor(historyEntityMap.get(clazz)).getTableName());
          }
     }

     private void customize(ClassDescriptor sourceDescriptor, String historyTableName) 
                                                                      throws Exception {
          String sourceTableName = (String) sourceDescriptor.getTableNames().get(
                    sourceDescriptor.getTableNames().size() - 1);
			
          HistoryPolicy policy = new HistoryPolicy();
          policy.addHistoryTableName(sourceTableName, historyTableName);
          policy.addStartFieldName(START_DATE);
          policy.addEndFieldName(END_DATE);
          sourceDescriptor.setHistoryPolicy(policy);
     }

     public abstract void buildHistoryEntityMap();
}

che quindi verrà usata in modo leggermente diverso da prima:

...
     public void buildHistoryEntityMap() {
          historyEntityMap.put(UserData.class, UserDataHist.class);
          historyEntityMap.put(User.class, UserDataHist.class);
          historyEntityMap.put(Account.class, AccountHist.class);
     }
...

I cambiamenti apportati quindi sono due:

  • è stata introdotta una mappa al posto della lista in modo da accoppiare le classi;
  • historyTableName adesso non viene costruita ma il suo nome viene recuperato dal descrittore dell’entità.

Per dormire la notte…

Vediamo adesso come eliminare la duplicazione del codice e risolvere il problema della foreign key. Riprendiamo l’entità UserData e creiamo una gerarchia di classi come segue:

Gerarchia UserData

Tutti i dati della vecchia UserData confluiranno nella classe AbstractUserData, ad eccezione degli attributi che creano vincoli tra entità, cioè quelli annotati con @OneToOne, @ManyToOne, @OneToMany e @ManyToMany. UserDataHist conterrà gli attributi data inizio/fine e i riferimenti agli indici delle entità a cui è legata. UserData avrà i riferimenti espliciti alle altre entità a cui è legata, cioè quelle relazione escluse da AbstractUserData.
La strategia JPA con cui annotiamo la relazione di gerarchia sarà table per class (ovvero una tabella per ogni classe concreta).

Qualche riga di codice forse rende tutto più chiaro ciò che abbiamo appena detto!

Definiamo la classe base che non verrà direttamente mappata sul database e contiene tutti gli attributi in comune alle due tabelle (ad esclusione dei riferimenti alle altre entità, come User).

@MappedSuperclass
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class AbstractUserData implements Serializable {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	protected long id;

	protected String address;
	
	protected String city;
	
	protected String state;
	
	protected int zip;
	
	@Column(name="phone_number")
	protected String phoneNumber;
	
	@Column(name="e_mail")
	protected String eMail;

...

}

La classe UserData estenderà la base e conterrà esclusivamente i riferimenti alle altre entità: EclipseLink provvederà a generare la foreing key user_id.

@Entity
@Table(name="user_data")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class UserData extends AbstractUserData {
	
        @OneToOne
        @JoinColumn(name="user_id", referencedColumnName="id")
        private User user;
...

}

La classe di storico, come ci si aspetta, estenderà la classe base a cui aggiungerà la data inizio/fine validità. Come si può notare più in basso è stato aggiunto anche l’attributo private long userId: in questo modo, ammesso che l’id di User sia di tipo long, viene creata la colonna user_id nella tabella user_data_hist, ma non viene aggiunta la foreign key su questa colonna.

@Entity
@Table(name="user_data_hist")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class UserDataHist extends AbstractUserData {
	
        @Id
	@Temporal(TemporalType.TIMESTAMP)
	@Column(name = "start_date")
	private Date startDate;

	@Temporal(TemporalType.TIMESTAMP)
	@Column(name = "end_date")
	private Date endDate;

        @Column(name="user_id")
        private long userId;
...

}

Con questo stratagemma si riesce a dire ad EclipseLink ciò che vogliamo esattamente e cioè due tabelle speculari, la seconda delle quali ha due campi in più e soprattutto non ha foreign key con il resto delle tabelle.

Conclusioni

Abbiamo visto che per risolvere il problema della generazione automatica di una tabella di storico abbiamo dovuto creare una entità nuova che la mappasse. Con questa soluzione abbiamo però introdotto e risolto due problemi: duplicazione di codice e generazione delle foreign key sulle tabelle di storico.
Oltre a risolvere quindi questi problemi ci troviamo ad aggirarne automaticamente un altro: avendo a disposizione l’entità di storico è possibile fare query più dettagliate nel tempo come per esempio:

  • specificare la validità di un’entità in un intervallo di tempo;
  • recuperare l’ultima modifica ad una entità (ovvero quella con data fine massima rispetto alle altre).

Ovviamente non è tutto oro quel che luccica! Le query infatti vengono eseguite sulla tabella di storico: nel nostro caso quindi come risultato avremo liste di UserDataHist e non di UserData!! Conviene quindi effettuare query direttamente nella tabella di storico solo quando se ne ha strettamente bisogno, altrimenti è meglio affidarsi alle varie tecniche già fornite da EclipseLink.

83 Posts

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 curriculum pubblico. Follow me on Twitter - LinkedIn profile - Google+