Skip to content

Spring Data JPA nous permet de créer des relations rapides et efficaces entre différentes entités. Pour les exemples qui suivent j'utiliserai les classes Chat et Personne tel que décrites ci-dessous.

@Entity
@Table(name = "chats")
public class Chat {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String nom;
    private String couleur;
    private int age;
}
@Entity
@Table(name = "personne")
public class Personne {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String nom;
    private String prenom;
    private String email;
    private int age;
}

Relation un à plusieurs (One To Many)

L'exemple suivant démontre la situation dans laquelle une personne peut avoir plusieurs chats, mais chaque chat a un seul propriétaire.


erDiagram

PERSONNE ||--o{ CHAT : "Possède"
  • On utilisera les décorateurs @OneToMany et @ManyToOne pour décrire le lien entre les entités.
  • Pour lire les décorateurs toujours se mettre dans la peau de l'entité courante et lire de gauche à droite.
  • Une personne a plusieurs chats devient @OneToMany

    • One Personne Has Many Chats
  • Dans la classe Chat on ajoute

    @ManyToOne
    @JoinColumn(
            name = "personne_id",
            nullable = true
    )
    private Personne personne;
    

  • Un chat peut être "possédé" par une seule personne, on conserve donc la clé étrangère dans cette classe (le @JoinColumn nomme la nouvelle colonne dans la BD)

  • Dans la classe Personne on ajoute

    @OneToMany(mappedBy = "personne")
    // Utilisation d'un set pour garantir qu'on n'a pas deux fois le même chat
    private Set<Chat> chats;
    

  • Une Personne a plusieurs chats et sera liée par la propriété personne dans un chat.

  • Ainsi, dans le code, on pourra appeler chat.personne pour obtenir les informations du propriétaire du chat.

  • On pourra également accéder à la liste des chats d'une personne en utilisant personne.chats

Relation un à un (OneToOne)

L'exemple suivant démontre la situation dans laquelle une personne peut avoir un seul chat et chaque chat a un seul propriétaire.


erDiagram

PERSONNE ||--o| CHAT : "Possède"
  • Semblable à la relation un à plusieurs.
  • On doit décider qui "possède" la clé étrangère. Dans ce cas-ci, je la laisse dans l'entité Chat.

  • Dans la classe Chat on ajoute

    @OneToOne
    @JoinColumn(
            name = "personne_id",
            nullable = true
    )
    private Personne personne;
    

  • Un chat peut être "possédé" par une seule personne et je choisis de garder la clé étrangère dans cette entité d'où le @JoinColumn

  • Dans la classe Personne on ajoute

    @OneToOne(mappedBy = "personne")
    private Chat chat;
    

  • On sait quel chat appartient à quelle personne grâce à la propriété personne de la classe Chat

Relation plusieurs à plusieurs (ManyToMany)

L'exemple suivant démontre la situation dans laquelle une personne peut avoir plusieurs chats et chaque chat peut avoir plusieurs propriétaires.


erDiagram

PERSONNE }|--|{ CHAT : "Adopte"

Évidemment, on devra créer une table pour la jointure


erDiagram

PERSONNE ||--|{ ADOPTION_CHAT : "Adopte"
CHAT ||--|{ ADOPTION_CHAT : "Est adopté"

JPA nous permet de faire la création de la table automatiquement avec la bonne configuration.

  • Encore une fois on doit décider dans quelle classe on crée le gros de la relation. Je le ferai dans Chat pour conserver le même format.
  • On ajoute le décorateur @ManyToMany sur les deux classes
  • Dans la classe Chat on remplacera @JoinColumn par @JoinTable
    // Classe Chat
    @ManyToMany
    @JoinTable(
            name = "adoption_chat",
            joinColumns = @JoinColumn(name = "chat_id"),
            inverseJoinColumns = @JoinColumn(name = "personne_id"),
            uniqueConstraints = @UniqueConstraint(columnNames = {"chat_id", "personne_id"})
    )
    private Set<Personne> personnes;
    
  • joinColumns = @JoinColumn(name = "chat_id") définit le nom de la colonne pour représenter la clé primaire de la classe dans laquelle on crée la table de jointure
  • inverseJoinColumns = @JoinColumn(name = "personne_id") définit le nom de la colonne pour représenter la clé primaire de la classe liée à celle-ci
  • @UniqueConstraint() permet de s'assurer qu'on ne combine pas plusieurs fois le même chat et propriétaire.
  • Dans l'exemple ci-dessus, la table adoption_chat sera crée et comportera 2 attributs, soit le id d'un chat (identifié par la colonne chat_id) et le id d'une personne (identifié par la colonne personne_id).

  • Dans la classe personne on ajoute le décorateur @ManyToMany

  • On lui indique par quelle propriété de la classe Chat elle doit faire le lien (dans ce cas-ci l'attribut personnes)
    // Classe Personne
    @ManyToMany(mappedBy = "personnes")
    private Set<Chat> chats;
    

Relation plusieurs à plusieurs complexe

Pour les relations plusieurs à plusieurs dans lesquelles on voudra conserver plus d'informations que les clés primaires des entités en relation, il faudra nous même créer une nouvelle entité pour en faire la représentation.

Continuons avec l'exemple précédent de relation plusieurs à plusieurs, mais on voudra également garder le moment ou l'adoption d'un chat a lieu.

  • Nous devrons créer une entité AdoptionChat.
  • Il faudra créer une nouvelle classe pour représenter la clé primaire de cette entité. Celle-ci regroupera les clés primaires des deux identités qu'on veut lier.
  • La classe représentant la clé primaire doit être marquée @Embeddable et implémenter l'interface Serializable ainsi que les méthodes Equals et HashCode (on tire avantage de Lombok pour cela)

    @EqualsAndHashCode
    @Embeddable
    public class AdoptionKey implements Serializable {
        @Column(name = "personne_id")
        private Long personneId;
        @Column(name = "chat_id")
        private int chatId;
    }
    

  • Ensuite on crée notre nouvelle entité qui utilisera cette classe com type pour sa clé primaire.

  • On établira également des relations @ManyToOne avec les entités liées (dans le cas présent une relation avec Chat et Personne)
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@Entity
public class AdoptionChat {
    @EmbeddedId
    private AdoptionKey id;

    // Relation avec Personne, @MapsId correspond à l'attribut de la clé primaire
    @ManyToOne
    @MapsId("personneId")
    @JoinColumn(name = "personne_id")
    private Personne personne;

    // Relation avec Chat, @MapsId correspond à l'attribut de la clé primaire
    @ManyToOne
    @MapsId("chatId")
    @JoinColumn(name = "chat_id")
    private Chat chat;

    @Column(name = "date_adoption")
    private Date dateAdoption;
}
  • Chacune des classe aura une relation @OneToMany correspondante
// Classe Chat
@OneToMany(mappedBy = "chat")
Set<AdoptionChat> adoptionsChat;


// Classe Personne
@OneToMany(mappedBy = "personne")
Set<AdoptionChat> adoptionsChat;

Assigner un objet dans une relation

  • Dans les services, marquer les méthodes qui modifient la base de données (ajouter, modifier, supprimer) avec le décorateur @Transactional. Ceci assure l'intégrité des données en cas d'échec de la méthode.

  • Même si la base de données conserve seulement la clé étrangère, on voudra manipuler les objets complets dans le code.

  • Par exemple, pour ajouter un propriétaire à un chat

// Extrait de ChatService.java
@Transactional
public void updateChat(Chat chat) {
    // Si aucun propriétaire n'est sélectionné
    if (chat.getPersonne().getId() == null) {
        chat.setPersonne(null);
    }
    else{
        // Récupérer l'objet Personne de la base de données
        Personne proprio = personneService.findById(chat.getPersonne().getId());
        // L'associer au Chat
        chat.setPersonne(proprio);
        // Sauvegarder dans la base de données
        chatRepository.save(chat);
    }
}
  • Il faudra gérer les erreurs potentielles dans le service également, par exemple le non respect des contraintes. On veut éviter de tenter une transaction à la base de données si on sait déjà que la transaction risque de ne pas réussir.

  • Exemple avec une relation un à un. Si la personne à laquelle on essaie d'assigner un chat en possède déjà un, on doit éviter de l'assigner.

@Transactional
public boolean updateChat(Chat chat) {
    if (chat.getPersonne().getId() == null) {
        chat.setPersonne(null);
        chatRepository.save(chat);
        return true;
    }
    else{
        Chat chatExistant = chatRepository.findChatByPersonneId(chat.getPersonne().getId());
        // Si aucun chat n'est trouvé, le propriétaire peut adopter
        if (chatExistant == null) {
            Personne proprio = personneService.findById(chat.getPersonne().getId());
            chat.setPersonne(proprio);
            chatRepository.save(chat);
            return true;
        }
        else {
            return false;
        }
    }
}