Il programmatore e le sue api

V parte: ancora sulla persistenza, due relazioni particolaridi

Nel nostro percorso per l‘ideazione e la creazione di una applicazione, prosegue l‘analisi delle varie tipologie di relazioni presenti nel modello delle entità e di come sia possibile realizzare il mapping.

Nell'articolo precedente sono state analizzate alcune delle principali relazioni descritte nel modello delle entità, mostrando come, partendo dalle considerazioni fatte in sede di analisi (le prime puntate di questa serie), fosse possibile passare alla implementazione del modello delle entità e renderlo persistente sul sistema di storage.
Per non tediare oltremodo il lettore, la volta scorsa ci eravamo lasciati tralasciando due casi particolari che invece per una trattazione esaustiva affronteremo in questo articolo. Si tratta delle relazioni Famiglia-Scheda e della relazione Apicoltore-Apiario: nel primo caso vedremo come sia possibile mappare una gerarchia di oggetti (legame padre-figlio) mentre nel secondo mostreremo come gestire una relazione qualificata.
Al solito il diagramma da cui partire è quello che riportiamo in ogni articolo, raffigurato in figura 1.

Figura 1 - Il modello delle entità.

 

La relazione Apicoltore-Apiario

Nella terza puntata di questa serie, nel paragrafo in cui venivano introdotte le varie entità e le relative relazioni, si dava anche una spiegazione del perche' e del percome delle varie definizioni. A tal proposito si ebbe modo di affrontare anche il caso delle due entità Apicoltore e Apiario che sono legate fra loro in modo piuttosto "lasco", proprio in funzione di quello che può accadere in uno scenario reale. Il tipo di attività che un apicoltore svolge in azienda porta alla creazione di un legame fra l'entità apicoltore e l'entità apiario in modo non esclusivo per nessuno dei due elementi coinvolti: ogni volta che l'apicoltore esegue un intervento in apiario, si instaura una relazione che viene mappata per mezzo di una entità apposita, l'entità intervento.
Si tratta quindi di un classico esempio di relazione qualificata, in cui la entitità Intervento serve per specificare l'associazione fra le due entità.
Per chiarire ulteriormente questo caso, è utile rispolverare quanto dice Luca Vetti Tagliati nel libro "UML e ingegneria del software" a proposito di questo tipo di relazione:

 

[...] In altre parole, in una relazione 1-a-n (o n-a-n) tra una Classe A e una B, fissato un determinato oggetto istanza della classe A, è necessario specificare un criterio in grado di individuare un preciso oggetto o un sottoinsieme di quelli associati, istanze dell'altra classe (B). Tali circostanze si prestano a essere modellate attraverso l'associazione qualificata (qualification), il cui nome è dovuto all'attributo, o alla lista di attributi, detti qualificatori, i cui valori permettono di partizionare l'insieme delle istanze associate a quella di partenza. La Classe A viene comunemente definita classe qualificata, mentre quella B è detta classe obiettivo. In un contesto implementativo, ai qualificatori si fa comunemente riferimento con il nome di indici. Si tratta di attributi che, tipicamente, appartengono alla relazione stessa e, meno frequentemente, alla classe oggetto della selezione. Nel primo caso, i valori sono specificati dalla classe che gestisce la generazione di nuovi link.

Nel nostro caso questo si traduce nel considerare il legame che si instaura fra apicoltore e apiario solo in concomitanza di un intervento che viene eseguito in un determinato giorno in funzione di una particolare operazione da svolgere. Di fatto la collezione delle entità Intervento rappresenta il log (o giornale di lavoro) con il quale memorizzare tutte le operazioni svolte dai vari apicoltori coinvolti.
Proseguendo con la teoria, Luca ci dice che:

 

L'utilizzo dell'associazione qualificata, sebbene possa creare qualche perplessità iniziale, ha un campo di azione piuttosto ben definito: è utilizzata in tutti quei casi in cui vi è una struttura di lookup da una parte della relazione. Ciò equivale anche a dire che, dal punto di vista dell'implementazione, la relazione dovrebbe essere realizzata per mezzo di opportuni oggetti che agevolino la ricerca, come Hashtable, Map, ecc. Caratteristica peculiare di tali oggetti è la realizzazione di un mapping tra un identificatore e un oggetto. Per esempio, la classe Hashtable in Java possiede i seguenti metodi per memorizzare e reperire oggetti:

 

put(key:Object, value:Object)
get(key:Object )

Pertanto le istanze di questa classe permettono di realizzare un mapping tra un oggetto che rappresenta la chiave (il qualificatore) e un altro che invece rappresenta ciò che si vuole individuare.

 

Quindi possiamo dire che questa relazione potrebbe essere mappata introducendo una variabile di tipo Hashtable nella classe Beekeeper: tale hashtable permette il reperimento delle varie istanza di Apiary utilizzando una istanza di Intervention come indice. In pratica si avrebbe una situazione come quella raffigurata in figura 2.



Figura 2 - In una tabella di hash, l'indice non deve essere necessariamente un oggetto semplice, come un int, ma potrebbe anche essere un oggetto strutturato. È questo il caso della relazione di associazione, dove l'oggetto associato alla relazione funge da indice nella tabella che memorizza tale associazione.

 

Questa implementazione, per quanto fedele alla modellazione OO, introduce un livello di complessità non facilmente risolvibile con gli attuali motori di persistenza. Il costrutto di "component" offerto da Hibernate potrebbe aiutare in qualche modo. Di certo in EJB 3 non vi sono soluzioni dirette e semplici a questo tipo di problema.
Normalmente, quando si presentano casi di questo tipo, conviene riconsiderare il problema cercando di capire se effettivamente quanto si sta cercando di risolvere è realmente necessario. Nel nostro caso potremmo "rifattorizzare" il modello (e quindi capire se l'analisi è modificabile senza stravolgere la natura del progetto), alterando lievemente il modo con cui l'informazione che lega apicoltore e apiario viene mantenuta nel sistema.
Se quello a cui siamo interessati è tenere traccia degli interventi che verranno eseguiti in apiario dai singoli apicoltori (ovvero tenere un diario degli interventi eseguiti da "chi" e "dove"), possiamo semplificare il modello, trasformando la relazione qualificata in una semplice relazione con entità di relazione: il legame quindi potrebbe essere disegnato come in figura 3.

Figura 3 - La relazione con qualificazione può essere riadattata in una relazione composta con entità centrale che funge da elemento di collegamento.

Di fatto l'entità Intervention in questo caso rappresenta la traduzione nel paradigma ad oggetti di quello che in un database è una tabella di cross (necessaria per mappare il legame n-a-m fra apicoltore e apiario): in questo caso si aggiungono alcuni attributi relativi alla associazione, che in gergo si chiamano attributi di link. Ogni istanza di Intervention quindi memorizza un legame fra Beekeeper e Apiary tenendo traccia anche della data di intervento e delle operazioni svolte.
Ecco il codice Java della implementazione secondo il modello EJB 3 (si notino le variabili Apiary e Beekeeper che legano l'entità Intervention con gli oggetti dipendenti):

@Entity
@Table(name = "intervention")
@NamedQueries({@NamedQuery(name = "Intervention.findById",
                query = "SELECT i FROM Intervention i WHERE i.id = :id"),
@NamedQuery(name = "Intervention.findByNotes",
        query = "SELECT i FROM Intervention i WHERE i.notes = :notes"),
@NamedQuery(name = "Intervention.findByDate",
        query = "SELECT i FROM Intervention i WHERE i.interventionDate = :date")})
public class Intervention implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @Column(name = "id", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    
    @Column(name = "notes")
    private String notes;
    
    @Column(name = "date")
    @Temporal(TemporalType.DATE)
    private Date interventionDate;
    
    @ManyToOne
    @JoinColumn(name = "apiary_id", referencedColumnName = "id")
    private Apiary apiary;
    
    @ManyToOne
    @JoinColumn(name = "beekeeper_id", referencedColumnName = "id")
    private Beekeeper beekeeper;
    
    public Intervention() {
}

Considerando il grafico di figura 3, si faccia attenzione alla tipologia di navigabilità: Apicoltore-Intervento è una relazione bidirezionale, mentre Intervento-Apiario è monodirezionale (da Intervento verso Apiario). Se la seconda classificazione trova piena corrispondenza nel modello di analisi, il primo caso è forse un po' meno giustificabile.

Il motivo di questa scelta è molto semplice: supponiamo di voler rendere la relazione bidirezionale, ovvero introducendo una variabile di relazione nella classe Beekeeper (che tenga quindi traccia della istanze di Intervention), tale variabile potrebbe essere poi mappata sul database secondo la tecnica del rivesciamento della chiave (vedi l'articolo precedente di questa serie), come mostrato nella seguente porzione di codice:

@OneToMany(mappedBy = "beekeeper")
private Collection interventions;

dove si dice che il legame è gestito dall'altro elemento della relazione. Questo comportamento apparentemente strano viene confermato da quanto riportato nella specifica:

L‘entità inversa di una relazione bidirezionale deve relazionarsi all‘entità a cui è riferita tramite l‘elemento mappedBy delle annotazioni OneToOne, OneToMany o ManyToMany. L‘elemento mappedBy designa la proprietà o il campo nell‘entità che è proprietaria della relazione.

Introducendo questo artefatto, è possibile inserire in Beekeeper il metodo

public Apiary getApiary(Intervention i){
    Apiary a = i.getApiary();
    return a;
}

Tale metodo, pur nella sua elementare e pleonastica funzionalità, permette di introdurre in Beekeeper la possibilità di reperire l'apiario su cui si è lavorato utilizzando una istanza di Intervention come indice: di fatto è la simulazione del modello hashtable di cui si è parlato poco sopra.

Si faccia attenzione che la presenza della variabile intervetions potrebbe rendere del tutto inutile la definizione del metodo getApiary(), dato che in ogni momento infatti potrebbe sempre essere possibile scrivere (N.B.: il metodo getElementAt() non è disponibile per la classe Collection, qui viene introdotto per semplificare la scrittura dell'articolo):

beekeeper.getInterventions().getElementAt(i).getApiary()

Questa scorciatoia permette di ricavare un apiario tramite l'indice dell'intervento, quindi con un risultato analogo, ma formalmente diverso.

Volendo impedire questa violazione, si potrebbe eliminare la variabile interventions: in questo caso il metodo getApiary() dovrebbe effettuare ogni volta una ricerca sullo storage per ricavare prima la particolare istanza di intervention e poi la apiary corrispondente:

public Apiary getApiary(Intervention i){
    // l'istanza em EntityManager viene ricavata come variabile
    // da un session bean di facade
    Intervention i = em.findIntervention(i.getId());
    Apiary a = i.getApiary();
    return a;
}

Questa opzione introduce un livello di complessità sicuramente maggiore dato dalla necessità di ricavare l'istanza di EntityManager per eseguire la ricerca: in pratica l'entity bean dovrebbe comportarsi come se fosse un session oppure, in alternativa, utilizzarne uno già presente nella applicazione con funzione di facade: la maggior parte dei lettori concorderanno con il fatto che questa sia una inutile complicazione. Il miglior compromesso fra ragione e fedeltà al modello potrebbe essere quello di optare per la introduzione della variabile interventions (in modo da avere a disposizione in Beekeeper la relazione con gli interventi effettuati dall'apicoltore), ma di proibirne l'uso definendola privata e non pubblicato il corrispondente metodo getIntervention().

Modellare la relazione Famiglia-Scheda

Veniamo al secondo caso, la relazione Famiglia con Scheda, entità che a sua volta instaura un legame generalizzazione-specializzazione con SchedaTrattamenti e SchedaSalute: in questo caso si deve gestire una relazione di ereditarietà. La letteratura ci dice che non esiste un solo modo per rappresentare questo genere di legame, essendo disponibili in genere le seguenti possibilità:

  • una tabella per entità
  • una sola tabella di mapping in fondo alla gerarchia con colonna discriminante
  • approccio misto

Queste sono tre alternative tutte valide e la scelta dipende dal contesto: da quanti attributi/relazioni ci sono in comune, da quanto bisogna rilassare i vincoli, dalle tipiche operazioni, e altro ancora.
Un modo per sciogliere ogni dubbio potrebbe essere quello di definire i vari attributi, magari specificando il legame per ogni variabile con la colonna della tabella/tabelle, in modo da capire cosa sia possibile portare a fattor comune o cosa invece impedisca una soluzione o un'altra.
Tanto per fornire un'idea, se si sceglie di utilizzare una sola tabella per tutte le entità si ottiene un indubbio risparmio di spazio (perche' si limita la ridondanza e la replica di informazioni comuni a entità diverse in diverse tabelle), ma si corre il rischio di non poter applicare tale tecnica se il database non permette di abbassare alcuni vincoli. Si prenda ad esempio lo schema di tabella 1:

Tabella 1

La seconda riga potrebbe rappresentare un'entità in fondo alla gerarchia (quindi con più attributi o colonne) mentre la prima un oggetto in alto nella scala gerarchica degli oggetti: dato che le colonne G H I non assumono alcun valore, quindi si può immaginare che la riga appartenga ad un oggetto che non ha tali attributi. La cosa non è così chiara perche' potrebbe trattarsi di un oggetto in fondo alla gerarchia in cui alcuni campi (ovviamente non obbligatori) non sono valorizzati. La terza riga evidenzia maggiormente questa possibilità.
Una semplificazione la si può avere introducendo una colonna che serva per memorizzare il tipo di record salvato, come si vede nella tabella 2:

Tabella 2

Appare chiaro che questo approccio, sebbene porti a una organizzazione dei dati molto compatta (usare una sola tabella non obbliga a ripetere informazioni su tabelle differenti), non è perseguibile nel caso in cui il database abbia regole e vincoli sulle colonne (ad esempio vieti che la colonna G possa essere nulla).
Rispetto alla soluzione antagonista (una tabella per ogni oggetto della gerarchia) è una soluzione più solida e resistente alle modifiche e refactoring sugli oggetti: una modifica alla classe base comporta solo l'aggiunta di una colonna alla tabella, mentre nell'altro caso si dovrebbero modificare molte tabelle.
Come appare ovvio non esiste una soluzione buona per tutte le stagioni, molti sono i fattori che entrano in gioco. A scopo didattico si è scelto di adottare la soluzione 2 (una tabella per tutte le entità con l'uso della colonna discriminante), in modo da mostrare come gli ORM possano consentire di gestire in modo automatico e intelligente questa situazione. Le tabelle necessarie per gestire questa relazione sono due: FAMILY e REPORT, assumendo che in REPORT mapperemo sia dati relativi a SchedaTrattamenti (entità TreatmentReport ) che SchedaSalute (entità HealthReport). Dal punto di vista dell'organizzazione degli oggetti Java la struttura vede alla base della gerarchia l'entity Report di cui è qui di seguito presentata la parte iniziale (quella relativa alla definizione degli attributi):

@Entity
@Table(name = "report")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="report_type")
@DiscriminatorValue(value="BAS")
@NamedQueries({@NamedQuery(name = "Report.findById",
            query = "SELECT r FROM Report r WHERE r.id = :id"),
            @NamedQuery(name = "Report.findByReportType",
                query = "SELECT r FROM Report r WHERE r.reportType = :reportType"),
            @NamedQuery(name = "Report.findAll",
                query = "SELECT r FROM Report r WHERE r.reportType = 'BAS'"),
            @NamedQuery(name = "Report.findByReportDate",
                query = "SELECT r FROM Report r WHERE r.reportDate = :reportDate")})
public class Report implements Serializable {
    public static final long serialVersionUID = 1L;
    public static final String BASEREPORTTYPE = "BASE";
    public static final String HEALTHREPORTTYPE = "HEALTH";
    public static final String TREATREPORTTYPE = "TREAT";
    
    @Id
    @Column(name = "id", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    protected Integer id;
    
    @Column(name = "report_type", updatable=false, insertable=false)
    private String reportType;
    
    @Lob
    @Column(name = "notes")
    protected String notes;
    
    
    @Column(name = "report_date")
    @Temporal(TemporalType.DATE)
    private Date reportDate;
    
    @OneToMany(mappedBy = "report")
    private Collection familyCollection;

La definizione della entità fa uso di particolari annotazioni che, oltre a specificare la tabella di mapping, permettono di specificare la strategia di mapping:

@Table(name = "report")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="report_type")
@DiscriminatorValue(value="BAS")

La annotazione Inheritance avverte il sistema che il bean appartiene a una gerarchia di oggetti e che la strategia adottata prevede una sola tabelle per tutta la gerarchia. La annotazione DiscriminatorColumn specifica quale colonna usare come discriminante; il valore scelto in questo caso è "BAS" (ovvero oggetto alla base delle gerarchia) come specificato dalla annotazione DiscriminatorValue. Quindi tutte le volte che si procede al salvataggio di una entità di questo tipo, nella colonna "report_type" l'application server (o meglio il motore di mapping), inserisce il valore "BAS".
Da notare che la definizione dell'oggetto Report come oggetto persistente non è del tutto aderente a quanto specificato in fase di analisi, dato che nel modello non dovrebbero esistere oggetti Report semplici, ma solo TreatmentReport ed HealthReport,: per questo motivo l'entità Report non dovrebbe mai essere resa persistente.
Volendo invece passare alla definizione delle due entità figlie, queste saranno definite in modo da non ripetere le definizioni per gli attributi base (con le relative regole di mapping) che sono invece presenti nell'oggetto padre. Di seguito è riportata la definizione della classe HealthReport (l'altro fratello ha una implementazione del tutto analoga):

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="report_type")
@DiscriminatorValue(value="HEA")
@NamedQueries({@NamedQuery(name = "HealthReport.findAll",
                    query = "SELECT h FROM HealthReport h"),
            @NamedQuery(name = "HealthReport.findById",
                query = "SELECT h FROM HealthReport h WHERE h.id = :id"),
            @NamedQuery(name = "HealthReport.findByReportDate",
                query = "SELECT h FROM HealthReport h WHERE h.reportDate = :reportDate")})
public class HealthReport extends Report implements Serializable {
    private static final long serialVersionUID = 1L;
    @Lob
    @Column(name = "health_description")
    private String healtDescription;
}

Si noti la presenza delle query in formato EJBQL che consentono la ricerca degli oggetti in base ai vari attributi delle classi: come era logico aspettarsi non è presente la query che consente la ricerca per l'attributo report_type, essendo un attributo discriminante (e per lo stesso motivo non è necessario definire la regola di persistenza di tale attributo, essendo gestita automaticamente dal framework): il fatto stesso di ricercare un HealthReport implica che si sta eseguendo una ricerca in cui il valore del camp report_type="HEA".

Conclusioni

In questo articolo abbiamo mostrato come si possano mappare due tipologie di relazione particolare. L'obiettivo al solito non era quello di affrontare in dettaglio la teoria delle varie tecniche di mapping, quanto mostrare, sulla base delle indicazioni scaturite in fase di analisi sia possibile passare alla progettazione ed implementazione nella maniera più fedele possibile.
In questo caso si è visto come, queste due tipologie di relazioni, per quanto siano realizzabili nel modello OO, portino con se alcune importanti implicazioni per quanto concerne la parte di mapping sul repository. In entrambi i casi si è visto come adottare un modello a oggetti senza alcun adattamento non sempre sia possibile o ottimale.
Nelle prossime puntate inizieremo a parlare dello strato di mezzo, ovvero del layer di business logic.

 

Condividi

Pubblicato nel numero
131 luglio 2008
Giovanni Puliti lavora come consulente nel settore dell’IT da oltre 20 anni. Nel 1996, insieme ad altri collaboratori crea MokaByte, la prima rivista italiana web dedicata a Java. Da allora ha svolto attività di formazione e consulenza su tecnologie JavaEE. Autore di numerosi articoli pubblicate sia su MokaByte.it che su…
Articoli nella stessa serie
Ti potrebbe interessare anche