mardi 22 février 2011

Relations sans foreign keys en Hibernate


En Hibernate, une relation entre deux entités s'exprime en générale à l'aide d'une foreign key. Néanmoins, de nombreux développeurs s'imaginent à tort qu'Hibernate a besoin d'une contrainte de foreign key entre les tables pour mapper une relation.

Certes, c'est une bonne idée. Prenons par exemple le cas de deux tables (MASTER et DOG, tables utilisées dans un précédent article). DOG ne contient que deux colonnes: ID et NAME. MASTER en contient trois: ID, NAME et DOG_ID, cette dernière contenant un ID de la table DOG.

Un schéma classique qui se mappe comme suit (cf. toujours le même article):
@Entity
public class Dog {
     @Id @GeneratedValue(strategy=GenerationType.AUTO)
     private Integer id;

     private String name;

     //Setters, getters, equals, hashcode...   
}
et
@Entity
public class Master {

     @Id @GeneratedValue(strategy=GenerationType.AUTO)
     private Integer id;

     private String name;

     @OneToOne
     private Dog dog;

     //setters getters, equals, hashcode    
}
Si on laisse à Hibernate le soin de générer le schéma, il ajoutera une contrainte FK sur la colonne DOG_ID pour que les valeurs qu'elle contient soient toujours des clés primaires de DOG.

Sans contrainte

Cependant, cette contrainte n'est absolument pas nécessaire et cela n'empêchera pas Hibernate de fonctionner (de la même manière, une propriété annotée @Id ne doit pas forcément correspondre à une clé primaire, mais c'est une autre histoire).

Curieusement, ce qui ressemble à une mauvaise pratique est plus courant qu'on ne le croit, même si elle ne se justifie souvent que par le poids de l'héritage (legacy).

Cela pose quand même un problème. Imaginons que notre DB contiennent les informations suivantes:
  • dans DOG, une ligne ID=1, NAME=Brutus
  • dans MASTER, une ligne ID=2, NAME=Toto, DOG_ID=1 et une ligne ID=3, NAME=Totor, DOG_ID=7

Nous avons donc une ligne master dont le DOG_ID ne référence aucun DOG.

Que va-t-il se passer avec le code suivant?
public class TestContrainte {
    public static void main(String[] args) {
        Configuration cfg = newAnnotationConfiguration().configure(); 
        SessionFactory sf = cfg.buildSessionFactory();
        Session session = sf.openSession();

        Master maitre = (Master)session.get(Maitre.class, 3);

        System.out.println(maitre.getNom());
        if(maitre.getChien() == null){ 
                System.out.println("Chien introuvable");
        } else {
                System.out.println("Chien: "+maitre.getChien().getNom());
        }

        session.close();
    }
} 

En fait, le code lance une exception:
Exception in thread "main" org.hibernate.ObjectNotFoundException: No row with the given identifier exists: [entities.Chien#7]

Evidemment, si on avait demandé l'id 2, on aurait obtenu la réponse:
Toto
Chien: Brutus

La solution

Le fait est qu'Hibernate considère que la référence vers Dog DOIT exister si une “FK” existe. Une exception est donc lancée. Ce comportement par défaut est généralement correct, mais dans les cas où la contrainte de foreign key n'existe pas, le code risque de planter.

Heusement, Hibernate propose un moyen de s'en sortir par le biais de ses annotations propres (hibernate-annotations).

Voici comment modifier la référence dans Master vers Dog:
@OneToOne
@NotFound(action=NotFoundAction.IGNORE)
private Dog dog;
L'annotation @org.hibernate.annotations.NotFound prend l'enum org.hibernate.annotations.NotFoundAction en paramètre. La valeur prise par défaut est NotFoundAction.EXCEPTION, ce qui correspond au comportement généralement observé. Mais quand "action" est en "IGNORE", comme dans ces quelques lignes, si Hibernate rencontre une "foreign key" ne pointant vers aucune ligne, il ne lancera pas d'exception, mais se contentera mettre la référence à null.

Ainsi donc, le test donné ci-dessus donnera comme résultat:
Totor
Chien introuvable

Ce qui n'est pas faux...

Aucun commentaire:

Enregistrer un commentaire