Participer au site avec un Tip
Rechercher
 

Améliorations / Corrections

Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.

Emplacement :

Description des améliorations :

La gestion des cascades

Exemple d'un mapping complet



Accès rapide :
Introduction à la notion de cascades
Cascader la persistance
Comportement sans la cascade
On cascade la persistance
Comportement avec la cascade de la persistance

Introduction à la notion de cascades

Comme nous l'avons du dans les chapitres précédents, les entités de votre modèle peuvent être mises en relation les unes avec les autres. Mais que se passe-t-il si vous engagez certaines actions (suppression, sauvegarde...) sur une grappe d'objets ? Par exemple, que se passe-t-il des lignes de commandes si je supprime la commande associée de la base de données ? Les lignes de commandes sont-elles aussi automatiquement supprimées de la base ? C'est ce type de comportement que vous pouvez contrôler grâce à la notion de « cascade ».

On définit les cascades que l'on souhaite voir être activées au niveau des annotations de relations. Pour ce faire, vous pouvez spécifier les types de cascades autorisés en utilisant le type énuméré CascadeType. En voici sa définition.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
/**
 * Defines the set of cascadable operations that are propagated
 * to the associated entity.
 * The value <code>cascade=ALL</code> is equivalent to
 * <code>cascade={PERSIST, MERGE, REMOVE, REFRESH, DETACH}</code>.
 *
 * @since Java Persistence 1.0
 */
public enum CascadeType {

    /** Cascade all operations */
    ALL,

    /** Cascade persist operation */
    PERSIST,

    /** Cascade merge operation */
    MERGE,

    /** Cascade remove operation */
    REMOVE,

    /** Cascade refresh operation */
    REFRESH,

    /**
     * Cascade detach operation
     *
     * @since Java Persistence 2.0
     *
     */
    DETACH
}
Définition du type énuméré CascadeType

Et voici comment appliquer un type de cascade à une relation de type @OneToMany.

 1 
 2 
 3 
 4 
 5 
 6 
@OneToMany( 
    targetEntity = CommandLine.class, 
    mappedBy = "command", 
    cascade = CascadeType.PERSIST
)
private Set<CommandLine> lines = new HashSet<>();
Utilisation du type énuméré CascadeType

Il est aussi possible d'associer plusieurs types de cascades à une même relation JPA en utilisant la syntaxe suivante :

 1 
 2 
 3 
 4 
 5 
 6 
@OneToMany(
    targetEntity = CommandLine.class,
    mappedBy = "command",
    cascade = { CascadeType.PERSIST, CascadeType.REMOVE }
)
private Set<CommandLine> lines = new HashSet<>();
Utilisation du type énuméré CascadeType

Cascader la persistance

Passons de suite à un exemple concret.

Comportement sans la cascade

Pour le moment, nous n'avons pas encore utilisé ce principe de cascade dans le mapping de l'exercice précédent (si vous êtes arrivé directement sur ce document vous pouvez le recopier de la page HTML correspondant au chapitre précédent). Que pensez-vous qu'il va se passer si on exécute le code suivant ?

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
package fr.koor.webstore;

import java.util.Date;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import fr.koor.webstore.business.Article;
import fr.koor.webstore.business.Command;
import fr.koor.webstore.business.CommandLine;
import fr.koor.webstore.business.User;

public class ConsoleCascade {

    public static void main(String[] args) throws Exception {
        EntityManagerFactory entityManagerFactory = null;
        EntityManager entityManager = null;
        try {
            entityManagerFactory = Persistence.createEntityManagerFactory("WebStore");
            entityManager = entityManagerFactory.createEntityManager();
        
            EntityTransaction trans = entityManager.getTransaction();
            trans.begin();
            
            User user = new User( "Bond", "James", 007 );

            Command theCommand = new Command( user, new Date() );
            Set<CommandLine> lines = theCommand.getLines();

            Article art1 = new Article( "Truc de fou", "La marque qui tue", 100 );
            Article art2 = new Article( "Truc de dingue", "Autre marque qui tue", 200 );
                    
            CommandLine line1 = new CommandLine( art1, theCommand, 2 );
            lines.add( line1 );
            CommandLine line2 = new CommandLine( art2, theCommand, 3 );
            lines.add( line2 );

            entityManager.persist( theCommand );
            
            System.out.println( theCommand );
                
            trans.rollback();
        } finally {
            if ( entityManager != null ) entityManager.close();
            if ( entityManagerFactory != null ) entityManagerFactory.close();
        }
    }
}
Exemple d'enregistrement d'une grappe d'objets sans prise en charge de la cascade

Comme vous le constatez, le programme se plante et produit les messages d'erreurs suivants.

[main] WARN  | org.hibernate.orm.connections.pooling                        | HHH10001002: Using Hibernate built-in connection pool (not for production use!) (DriverManagerConnectionProviderImpl.java:70)
[main] WARN  | org.hibernate.action.internal.UnresolvedEntityInsertActions  | HHH000437: Attempting to save one or more entities that have a non-nullable association with an unsaved transient entity. The unsaved transient entity must be saved in an operation prior to saving these dependent entities.
    Unsaved transient entity: ([fr.koor.webstore.business.User#0])
    Dependent entities: ([[fr.koor.webstore.business.Command#<null>]])
    Non-nullable association(s): ([fr.koor.webstore.business.Command.user]) (UnresolvedEntityInsertActions.java:144)
[main] WARN  | org.hibernate.action.internal.UnresolvedEntityInsertActions  | HHH000437: Attempting to save one or more entities that have a non-nullable association with an unsaved transient entity. The unsaved transient entity must be saved in an operation prior to saving these dependent entities.
    Unsaved transient entity: ([fr.koor.webstore.business.Article#0])
    Dependent entities: ([[fr.koor.webstore.business.CommandLine#<null>]])
    Non-nullable association(s): ([fr.koor.webstore.business.CommandLine.article]) (UnresolvedEntityInsertActions.java:144)
[main] WARN  | org.hibernate.action.internal.UnresolvedEntityInsertActions  | HHH000437: Attempting to save one or more entities that have a non-nullable association with an unsaved transient entity. The unsaved transient entity must be saved in an operation prior to saving these dependent entities.
    Unsaved transient entity: ([fr.koor.webstore.business.Article#0])
    Dependent entities: ([[fr.koor.webstore.business.CommandLine#<null>]])
    Non-nullable association(s): ([fr.koor.webstore.business.CommandLine.article]) (UnresolvedEntityInsertActions.java:144)
[main] WARN  | org.hibernate.action.internal.UnresolvedEntityInsertActions  | HHH000437: Attempting to save one or more entities that have a non-nullable association with an unsaved transient entity. The unsaved transient entity must be saved in an operation prior to saving these dependent entities.
    Unsaved transient entity: ([fr.koor.webstore.business.Command#0])
    Dependent entities: ([[fr.koor.webstore.business.CommandLine#<null>]])
    Non-nullable association(s): ([fr.koor.webstore.business.CommandLine.command]) (UnresolvedEntityInsertActions.java:144)
[main] ERROR | org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl | Collection leak detected: there are 1 unclosed connections upon shutting down pool jdbc:mariadb://localhost/WebStore (PooledConnections.java:92)
Exception in thread "main" java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved beforeQuery current operation : fr.koor.webstore.business.CommandLine.article -> fr.koor.webstore.business.Article
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:144)
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:155)
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:162)
    at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:777)
    at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:743)
    at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:748)
    at fr.koor.webstore.ConsoleCascade.main(ConsoleCascade.java:41)
Caused by: org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved beforeQuery current operation : fr.koor.webstore.business.CommandLine.article -> fr.koor.webstore.business.Article
    at org.hibernate.action.internal.UnresolvedEntityInsertActions.checkNoUnresolvedActionsAfterOperation(UnresolvedEntityInsertActions.java:122)
    at org.hibernate.engine.spi.ActionQueue.checkNoUnresolvedActionsAfterOperation(ActionQueue.java:418)
    at org.hibernate.internal.SessionImpl.checkNoUnresolvedActionsAfterOperation(SessionImpl.java:613)
    at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:774)
    ... 3 more

Le problème vient du fait qu'on cherche à persister l'instance de commande alors que ses dépendances (les lignes de commandes, entre autres) ne sont pas encore persistées. Bien entendu, vous auriez pu persister chaque nouvel objet au fur et à mesure de leurs constructions, mais cela aurait occupé plus de code. La bonne solution consiste plutôt à activer la cascade sur la persistance.

On cascade la persistance

Pour que l'exemple précédent fonctionne il faut donc modifier le mapping comme indiqué ci-dessous : je ne présente que les lignes de code relatives au mapping.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
@Entity @Table(name="T_CommandLines")
public class CommandLine {

    @Id @GeneratedValue( strategy = GenerationType.IDENTITY )
    private int idCommandLine;
    
    @ManyToOne( cascade = CascadeType.PERSIST )
    @JoinColumn( name = "IdCommand", nullable = false )
    private Command command;
    
    @ManyToOne( cascade = CascadeType.PERSIST ) 
    @JoinColumn( name = "IdArticle", nullable = false )
    private Article article;
    
    private int quantity;
 
    // Suite de la définition de la classe
    
}
Modification du mapping de la classe CommandLine
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
@Entity  @Table(name="T_Commands")
public class Command {

    @Id @GeneratedValue( strategy = GenerationType.IDENTITY )
    private int idCommand;
    
    @ManyToOne( cascade = CascadeType.PERSIST ) 
    @JoinColumn( name = "IdUser", nullable = false )
    private User user;
    
    private Date commandDate;
    
    @OneToMany( 
            targetEntity = CommandLine.class, 
            mappedBy = "command", 
            cascade = { CascadeType.PERSIST, CascadeType.REMOVE }
    )
    private Set<CommandLine> lines = new HashSet<CommandLine>();
    
    @OneToOne( mappedBy = "command" )
    private Payment payment;

    // Suite de la définition de la classe
    
}
Modification du mapping de la classe Command

Comportement avec la cascade de la persistance

Il ne reste plus qu'à relancer l'application et vous devriez constater les résultats suivants.

[main] WARN  | org.hibernate.orm.connections.pooling                        | HHH10001002: Using Hibernate built-in connection pool (not for production use!) (DriverManagerConnectionProviderImpl.java:70)
[main] DEBUG | org.hibernate.SQL                                            | insert into T_Users (connectionNumber, login, password) values (?, ?, ?) (SqlStatementLogger.java:92)
[main] DEBUG | org.hibernate.SQL                                            | insert into T_Commands (commandDate, IdUser) values (?, ?) (SqlStatementLogger.java:92)
[main] DEBUG | org.hibernate.SQL                                            | insert into T_Articles (brand, description, UnitaryPrice) values (?, ?, ?) (SqlStatementLogger.java:92)
[main] DEBUG | org.hibernate.SQL                                            | insert into T_CommandLines (IdArticle, IdCommand, quantity) values (?, ?, ?) (SqlStatementLogger.java:92)
[main] DEBUG | org.hibernate.SQL                                            | insert into T_Articles (brand, description, UnitaryPrice) values (?, ?, ?) (SqlStatementLogger.java:92)
[main] DEBUG | org.hibernate.SQL                                            | insert into T_CommandLines (IdArticle, IdCommand, quantity) values (?, ?, ?) (SqlStatementLogger.java:92)
Commande de >> 10: Bond/James - 7 connexion(s) - Wed Feb 26 17:25:29 CET 2020
    14: 3 x [23]: truc de dingue de marque AUTRE MARQUE QUI TUE - 200.0 euros
    15: 2 x [24]: truc de fou de marque LA MARQUE QUI TUE - 100.0 euros
    Prix total de la commande : 800.0 euros
si vous supprimer une commande, les lignes de commandes devraient être elles aussi supprimées, étant donné que j'ai aussi demandé à cascader la suppression d'entités. Par contre, l'utilisateur associé (qui peut avoir d'autres commandes) et les articles du catalogue resteront existant.


Exemple d'un mapping complet