Capitolo 16. Esempio: Genitore/Figlio (Parent/Child)

Una delle primissime cose che i nuovi utenti tentano di fare con Hibernate è modellare una relazione di tipo genitore / figlio. Ci sono due approcci differenti, per farlo. Per varie ragioni, l'approccio più conveniente, soprattutto per i neofiti, è modellare sia Parent sia Child come classi di entità con una associazione <one-to-many> da Parent a Child. (L'approccio alternativo è dichiarare il Child come un <composite-element>.) Ora, la semantica predefinita di una associazione uno-a-molti in Hibernate è molto meno affine alla semantica usuale di una relazione genitore - figlio di quanto non lo sia quella di un mappaggio ad elemento composito. Mostreremo ora come usare una associazione uno a molti bidirezionale e con cascate per modellare una relazione genitore / figlio in maniera efficiente. Non è per niente difficile!

16.1. Una nota sulle collezioni

Le collezioni di Hibernate vengono considerate logicamente parte della entità che le possiede, e mai delle entità contenute. Questa è una precisazione cruciale, che ha le seguenti conseguenze:

  • Quando rimuoviamo / aggiungiamo un oggetto da / a una collezione, il numero di versione del proprietario viene incrementato.

  • Se un oggetto che è stato rimosso da una collezione è un'istanza di un tipo di valore ("value type"), cioè un elemento composito, quell'oggetto cesserà di essere persistente e il suo stato verrà completamente rimosso dal database. Nello stesso modo, aggiungendo una istanza di un tipo di valore alla collezione causerà il fatto che il suo stato sarà reso persistente.

  • Dall'altro lato, se un'entità viene rimossa da una collezione (che sia associata uno-a-molti o molti-a-molti), non verrà cancellata, come funzionamento predefinito. Questo comportamento è del tutto coerente - un cambiamento allo stato interno di un'altra entità non dovrebbe causare il fatto che l'entità associata svanisca! Nello stesso modo, l'aggiunta di un'entità a una collezione non causa il fatto che quell'entità venga automaticamente resa persistente (nel comportamento predefinito).

Invece, il comportamento standard prevede che aggiungere un'entità a una collezione si limiti a creare un collegamento tra le due entità, così come rimuoverla determinerà la rimozione di quel collegamento. Questo funzionamento è il più appropriato per moltissimi casi, mentre non è appropriato per nulla nel caso di una relazione genitore / figlio in cui la vita del figlio sia legata al ciclo di vita del genitore.

16.2. Uno-a-molti bidirezionale

Supponete che cominciamo con una semplice associazione <one-to-many> da Parent a Child.

<set name="children">
    <key column="parent_id"/>
    <one-to-many class="Child"/>
</set>

Se dovessimo eseguire il codice seguente

Parent p = .....;
Child c = new Child();
p.getChildren().add(c);
session.save(c);
session.flush();

Hibernate produrrebbe le due istruzioni SQL che seguono:

  • una INSERT per creare il record per c

  • una UPDATE per creare il collegamento da p a c

Questo non solo è inefficiente, ma viola anche i vincoli NOT NULL sulla colonna parent_id.

La causa soggiacente è che il collegamento (la chiave esterna parent_id) da p a c non viene considerata parte dello stato dell'oggetto Child è quindi non viene creata nell'istruzione INSERT. La soluzione è quindi fare in modo che il collegamento sia parte del mappaggio di Child.

<many-to-one name="parent" column="parent_id" not-null="true"/>

(Abbiamo anche bisogno di aggiungere la proprietà parent sulla classe Child.)

Ora che l'entità Child gestisce lo stato del collegamento, diciamo alla collezione di non aggiornarlo. Usiamo quindi l'attributo inverse.

<set name="children" inverse="true">
    <key column="parent_id"/>
    <one-to-many class="Child"/>
</set>

Per aggiungere un nuovo Child verrebbe allora usato il codice seguente:

Parent p = (Parent) session.load(Parent.class, pid);
Child c = new Child();
c.setParent(p);
p.getChildren().add(c);
session.save(c);
session.flush();

E ora verrà generata una sola INSERT SQL!

Per facilitare un po' le cose, possiamo creare un metodo addChild() al Parent.

public void addChild(Child c) {
    c.setParent(this);
    children.add(c);
}

A questo punto il codice per aggiungere un Child appare così:

Parent p = (Parent) session.load(Parent.class, pid);
Child c = new Child();
p.addChild(c);
session.save(c);
session.flush();

16.3. Ciclo di vita con cascate

La chiamata esplicita a save() ci infastidisce ancora. Abbiamo quindi bisogno di gestire la situazione usando le cascate.

<set name="children" inverse="true" cascade="all">
    <key column="parent_id"/>
    <one-to-many class="Child"/>
</set>

Questo semplifica il codice seguente in questo modo:

Parent p = (Parent) session.load(Parent.class, pid);
Child c = new Child();
p.addChild(c);
session.flush();

In maniera similare, non abbiamo bisogno di iterare sui figli per salvare o cancellare un Parent. Quanto segue rimuove p e tutti i suoi figli dal database.

Parent p = (Parent) session.load(Parent.class, pid);
session.delete(p);
session.flush();

Però il codice seguente

Parent p = (Parent) session.load(Parent.class, pid);
Child c = (Child) p.getChildren().iterator().next();
p.getChildren().remove(c);
c.setParent(null);
session.flush();

non rimuoverà ancora c from the database; rimuoverà solo il link verso p (e causerà la violazione di vincolo NOT NULL, in questo caso). C'è bisogno di cancellare esplicitamente (delete()) il Child.

Parent p = (Parent) session.load(Parent.class, pid);
Child c = (Child) p.getChildren().iterator().next();
p.getChildren().remove(c);
session.delete(c);
session.flush();

Ora, nel nostro caso un Child non può esistere senza il suo genitore. Quindi se rimuoviamo un Child dalla collezione, vogliamo che venga cancellato davvero. Per questo, dobbiamo usare cascade="all-delete-orphan".

<set name="children" inverse="true" cascade="all-delete-orphan">
    <key column="parent_id"/>
    <one-to-many class="Child"/>
</set>

Nota: anche se il mappaggio della collezione specifica inverse="true", le cascate sono comunque gestite iterando sugli elementi della collezione. Quindi, se avete bisogno che un oggetto venga salvato, cancellato, o aggiornato per cascata, dovete aggiungerlo alla collezione. Non è sufficiente chiamare solo setParent().

16.4. Come utilizzare update() in cascata

Immaginate che carichiamo un Parent in una Session, facciamo qualche cambiamento ad una azione di interfaccia e vogliamo rendere persistenti questi cambiamenti in una nuova Session (chiamando update()). Il Parent conterrà una collezione di figli e, poiché è abilitato l'aggiornamento in cascata, Hibernate ha bisogno di sapere quali figli siano appena stati istanziati, e quali invece rappresentino righe già esistenti nel database. Assumiamo che sia il Parent sia il Child abbiano proprietà di identificazione (sintetiche) di tipo java.lang.Long. Hibernate userà il valore della proprietà identificatore per determinare quali dei figli sono nuovi. (Potete anche usare le proprietà versione o marca di tempo (timestamp), vedete Sezione 9.4.2, “Aggiornamento di oggetti sganciati”.)

L'attributo unsaved-value viene usato per specificae il valore di identificatore di una istanza appena creata. Se non specificato, unsaved-value vale "null", il che è perfetto, per un identificatore di tipo Long. Se avessimo usato una proprietà di identificazione di un tipo primitivo, dovremmo specificare

<id name="id" type="long" unsaved-value="0">

per il mappaggio del Child. (C'è anche un attributo unsaved-value per i mappaggi di proprietà di versione e timestamp.)

Il codice seguente aggiornerà il parent e child e inserirà newChild.

//parent e child sono già stati caricati in una sessione precedente
parent.addChild(child);
Child newChild = new Child();
parent.addChild(newChild);
session.update(parent);
session.flush();

Bene, questo è perfetto per il caso in cui si abbia un identificatore generato automaticamente, ma cosa succede quando si hanno identificatori assegnati manualmente e identificatori composti? In questo caso è più difficile, perché unsaved-value non può distinguere tra un oggetto appena istanziato (con identificatore assegnato dall'utente) e un oggetto caricato in una sessione precedente). In questi casi, avrete probabilmente bisogno di dare una mano ad Hibernate, o

  • definendo unsaved-value="null" o unsaved-value="negative" su una proprietà <version> o <timestamp> per la classe.

  • impostare unsaved-value="none" e salvare esplicitamente (con save()) i figli appena istanziati prima di chiamare update(parent)

  • impostare unsaved-value="any" ed aggiornare esplicitamente (con update()) i figli precedentemente resi persistenti prima di chiamare update(parent)

none è il valore unsaved-value predefinito per gli identificatori assegnati e composti.

C'è una possibilità ulteriore. C'è un nuovo metodo sulla classe Interceptor che si chiama isUnsaved() che consente all'applicazione di implementare la propria strategia per distinguere gli oggetti appena istanziati. Ad esempio, potreste definire una classe di base per le vostre classi persistenti.

public class Persistent {
    private boolean _saved = false;
    public void onSave() {
        _saved=true;
    }
    public void onLoad() {
        _saved=true;
    }
    ......
    public boolean isSaved() {
        return _saved;
    }
}

(La proprietà saved è non-persistente.) Ora implementate isUnsaved() insieme a onLoad() e onSave() come segue:

public Boolean isUnsaved(Object entity) {
    if (entity instanceof Persistent) {
        return new Boolean( !( (Persistent) entity ).isSaved() );
    }
    else {
        return null;
    }
}

public boolean onLoad(Object entity, 
    Serializable id,
    Object[] state,
    String[] propertyNames,
    Type[] types) {

    if (entity instanceof Persistent) ( (Persistent) entity ).onLoad();
    return false;
}

public boolean onSave(Object entity,
    Serializable id,
    Object[] state,
    String[] propertyNames,
    Type[] types) {
        
    if (entity instanceof Persistent) ( (Persistent) entity ).onSave();
    return false;
}

16.5. Conclusione

Ci sono vari concetti da digerire, qui, e potrebbe sembrare confuso, in un primo momento. Comunque, nella pratica funziona tutto molto bene. La maggior parte delle applicazioni basate su Hibernate usando il pattern genitore / figlio in vari posti.

Abbiamo menzionato un'alternativa nel primo paragrafo. Nessuna delle questioni precedenti esiste nel caso di mappaggi con <composite-element>, che hanno esattamente la semantica di una relazione padre / figlio. Sfortunatamente ci sono due grosse limitazioni per gli elementi composti: non possono avere collezioni, e non dovrebbero essere figli di un'entità diversa dal loro genitore unico. (Per quanto, possano avere una chiave primaria surrogata usando il mappaggio <idbag>.)