Mappage d'une seule entité à plusieurs tables dans JPA

Haut de persistance

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS

1. Introduction

JPA simplifie la gestion des modèles de bases de données relationnelles de nos applications Java. Les choses sont simples lorsque nous mappons chaque table à une seule classe d'entité. Mais, parfois, nous avons des raisons de modéliser nos entités et nos tables différemment:

  • Lorsque nous voulons créer des groupes logiques de champs, nous pouvons mapper plusieurs classes sur une seule table
  • Si l'héritage est impliqué, nous pouvons mapper une hiérarchie de classes à une structure de table
  • Dans les cas où les champs liés sont dispersés entre plusieurs tables et que nous voulons modéliser ces tables avec une seule classe

Dans ce court tutoriel, nous verrons comment aborder ce dernier scénario.

2. Modèle de données

Disons que nous gérons un restaurant et que nous souhaitons stocker des données sur chaque repas que nous servons:

  • Nom
  • la description
  • prix
  • quel type d'allergènes il contient

Comme il existe de nombreux allergènes possibles, nous allons regrouper cet ensemble de données. De plus, nous allons également modéliser cela en utilisant les définitions de tableau suivantes:

Voyons maintenant comment mapper ces tables à des entités à l'aide d'annotations JPA standard.

3. Création de plusieurs entités

La solution la plus évidente est de créer une entité pour les deux classes.

Commençons par définir l' entité Meal :

@Entity @Table(name = "meal") class Meal { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") Long id; @Column(name = "name") String name; @Column(name = "description") String description; @Column(name = "price") BigDecimal price; @OneToOne(mappedBy = "meal") Allergens allergens; // standard getters and setters }

Ensuite, nous ajouterons l' entité Allergens :

@Entity @Table(name = "allergens") class Allergens { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "meal_id") Long mealId; @OneToOne @PrimaryKeyJoinColumn(name = "meal_id") Meal meal; @Column(name = "peanuts") boolean peanuts; @Column(name = "celery") boolean celery; @Column(name = "sesame_seeds") boolean sesameSeeds; // standard getters and setters }

Dans l'exemple ci-dessus, nous pouvons voir que repas_id est à la fois la clé primaire et également la clé étrangère. Cela signifie que nous devons définir la colonne de relation un-à-un à l'aide de @PrimaryKeyJoinColumn .

Cependant, cette solution présente deux problèmes:

  • Nous voulons toujours conserver les allergènes pour un repas, et cette solution n'applique pas cette règle
  • Les données sur les repas et les allergènes vont ensemble de manière logique - par conséquent, nous pourrions vouloir stocker ces informations dans la même classe Java même si nous avons créé plusieurs tables pour eux

Une solution possible au premier problème consiste à ajouter l' annotation @NotNull au champ allergènes de notre entité Repas . JPA ne nous laissera pas persister le repas si nous avons un allergène nul .

Cependant, ce n'est pas une solution idéale; nous en voulons un plus restrictif, où nous n'avons même pas la possibilité d'essayer de persister un repas sans allergènes.

4. Création d'une seule entité avec @SecondaryTable

Nous pouvons créer une seule entité en spécifiant que nous avons des colonnes dans différentes tables à l'aide de l' annotation @SecondaryTable :

@Entity @Table(name = "meal") @SecondaryTable(name = "allergens", pkJoinColumns = @PrimaryKeyJoinColumn(name = "meal_id")) class Meal { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") Long id; @Column(name = "name") String name; @Column(name = "description") String description; @Column(name = "price") BigDecimal price; @Column(name = "peanuts", table = "allergens") boolean peanuts; @Column(name = "celery", table = "allergens") boolean celery; @Column(name = "sesame_seeds", table = "allergens") boolean sesameSeeds; // standard getters and setters }

Dans les coulisses, JPA joint la table principale à la table secondaire et remplit les champs. Cette solution est similaire à la relation @OneToOne , mais de cette façon, nous pouvons avoir toutes les propriétés dans la même classe.

Il est important de noter que si nous avons une colonne qui est dans une table secondaire, nous devons la spécifier avec l' argument table de l' annotation @Column . Si une colonne se trouve dans la table primaire, nous pouvons omettre l' argument table car JPA recherche par défaut des colonnes dans la table primaire.

Notez également que nous pouvons avoir plusieurs tables secondaires si nous les intégrons dans @SecondaryTables . Alternativement, à partir de Java 8, nous pouvons marquer l'entité avec plusieurs annotations @SecondaryTable car il s'agit d'une annotation répétable.

5. Combinaison de @SecondaryTable avec @Embedded

Comme nous l'avons vu, @SecondaryTable mappe plusieurs tables à la même entité. Nous savons aussi que @Embedded et @ intégrable à faire le contraire et carte une seule table à plusieurs classes.

Voyons ce que nous obtenons lorsque nous combinons @SecondaryTable avec @Embedded et @Embeddable :

@Entity @Table(name = "meal") @SecondaryTable(name = "allergens", pkJoinColumns = @PrimaryKeyJoinColumn(name = "meal_id")) class Meal { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") Long id; @Column(name = "name") String name; @Column(name = "description") String description; @Column(name = "price") BigDecimal price; @Embedded Allergens allergens; // standard getters and setters } @Embeddable class Allergens { @Column(name = "peanuts", table = "allergens") boolean peanuts; @Column(name = "celery", table = "allergens") boolean celery; @Column(name = "sesame_seeds", table = "allergens") boolean sesameSeeds; // standard getters and setters }

C'est une approche similaire à ce que nous avons vu en utilisant @OneToOne . Cependant, il présente quelques avantages:

  • JPA gère les deux tables ensemble pour nous, nous pouvons donc être sûrs qu'il y aura une ligne pour chaque repas dans les deux tables
  • De plus, le code est un peu plus simple, car nous avons besoin de moins de configuration

Néanmoins, cette solution de type un-à-un ne fonctionne que lorsque les deux tables ont des identifiants correspondants.

Il est à noter que si nous voulons réutiliser la classe Allergens , il vaudrait mieux définir les colonnes de la table secondaire dans la classe Meal avec @AttributeOverride .

6. Conclusion

Dans ce court didacticiel, nous avons vu comment nous pouvons mapper plusieurs tables à la même entité à l'aide de l' annotation JPA @SecondaryTable .

Nous avons également vu les avantages de combiner @SecondaryTable avec @Embedded et @Embeddable pour obtenir une relation similaire à un-à-un.

Comme d'habitude, les exemples sont disponibles à l'adresse over sur GitHub.

Fond de persistance

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS