Les clés primaires avec JPA

Java
I211
Lecture
JPA
contrôle fin des identifiants JPA.
Auteur
Affiliations

Université de Toulon

LIS UMR CNRS 7020

Date de publication

2025-01-30

1 Softwares

Source
Branch
  • develop (c88a48e)
  • 2024/03/08 11:41:10
Java
  • OpenJDK Temurin-21.0.5+11
  • Apache Maven 3.9.9
Docker
  • Client: 27.3.1 - buildx v0.18.0 - compose v2.30.3
  • Server: 27.3.1

2 Concepts fondamentaux

  • Clé primaire
    • sous-ensemble minimal K d’attributs A = {a₁, a₂, …, aₙ} d’une relation R qui satisfait simultanément :
      1. l’unicité, qui garantit que pour tous tuples t₁, t₂ ∈ R, si t₁ ≠ t₂ alors leurs projections sur K sont différentes (t₁[K] ≠ t₂[K]),
      2. la minimalité, qui assure qu’aucun sous-ensemble propre K’ ⊂ K ne peut satisfaire la propriété d’unicité,
      3. la non-nullité, qui impose que pour tout tuple t ∈ R et tout attribut k ∈ K, la valeur t[k] ne peut pas être nulle (t[k] ≠ null),
  • Types supportés: primitifs (int, long, …), wrappers (Integer, Long, …), String, Date (Date, LocalDate, …), UUID
  • Annotation @Id sur l’attribut de l’entité

3 Génération automatique des Id atomiques

En théorie des bases de données relationnelles, une clé primaire est définie comme un sous-ensemble minimal K d’attributs A = {a₁, a₂, …, aₙ} d’une relation R qui satisfait simultanément trois propriétés fondamentales : (1) l’unicité, qui garantit que pour tous tuples t₁, t₂ ∈ R, si t₁ ≠ t₂ alors leurs projections sur K sont différentes (t₁[K] ≠ t₂[K]), (2) la minimalité, qui assure qu’aucun sous-ensemble propre K’ ⊂ K ne peut satisfaire la propriété d’unicité, et (3) la non-nullité, qui impose que pour tout tuple t ∈ R et tout attribut k ∈ K, la valeur t[k] ne peut pas être nulle (t[k] ≠ null), ces propriétés garantissant ainsi l’intégrité des entités et l’identification unique des tuples dans la base de données.

La génération automatique des identifiants est une fonctionnalité essentielle de JPA, permettant de déléguer la création des valeurs d’identifiants au framework.

Plusieurs stratégie de génération sont disponibles, chacune adaptée à un contexte particulier.

  1. IDENTITY
    • Utilise l’auto-increment natif de la base
    • Optimal pour MySQL/MariaDB
    • Pas de batch possible
  2. SEQUENCE
    • Utilise une séquence en base
    • Performant avec allocationSize > 1
    • Standard pour PostgreSQL/Oracle
  3. TABLE
    • Utilise une table dédiée
    • Portable mais moins performant
    • Solution de dernier recours
  4. AUTO
    • Choix automatique selon la base
    • IDENTITY pour MySQL
    • SEQUENCE pour PostgreSQL
  5. UUID
    • Utilise un UUID
    • Portable et unique

3.1 Tableau comparatif

Stratégie Performance Portabilité Batch Scalabilité Usage recommandé
IDENTITY +++ + Non + MySQL/MariaDB
SEQUENCE ++ ++ Oui ++ PostgreSQL/Oracle
TABLE + +++ Oui +++ Multi-bases
AUTO ++ ++ Selon ++ Développement
UUID + +++ Non ++++ Systèmes distribués

Notes sur la scalabilité:

  • IDENTITY: problématique en sharding
  • SEQUENCE: possible avec sequences par shard
  • TABLE: adapté au clustering
  • UUID: optimal pour systèmes distribués

3.2 Les générateurs SEQUENCE et IDENTITY

  • Les générateurs SEQUENCE et IDENTITY spécifient l’utilisation d’une séquence de base de données ou d’une colonne d’identité (plus performant, mais pas portable). .
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Getter

@Entity(name = "CustomerSeqId")
@Table(name="CUSTOMER", schema = "EX_IDSEQ")
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_customer")
    @SequenceGenerator(name = "seq_customer", sequenceName = "CUSTOMER_SEQUENCE", allocationSize = 10)
    private Long id;

    private String name;
}
Figure 1: EmployeePK.java
Figure 2: An Employee with a composite Key/Id

3.3 Sequence et Batch

La génération de clés avec une séquence peut être optimisée en utilisant un allocationSize plus grand que 1. Cela permet de pré-allouer un bloc de valeurs pour les entités, réduisant ainsi le nombre d’appels à la séquence.

// Exemple de configuration optimisée
@Entity
public class Order {
    @Id 
    @GeneratedValue(
        strategy = GenerationType.SEQUENCE,
        generator = "order_seq"
    )
    @SequenceGenerator(
        name = "order_seq",
        sequenceName = "ORDER_SEQ",
        initialValue = 1,
        allocationSize = 50  // Optimisation batch
    )
    private Long id;
}

3.4 générateur TABLE

Le générateur TABLE demande d’attribuer des clés primaires à l’entité en utilisant une table de base de données, c’est le plus portable mais le moins performant.

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Getter

@Entity(name = "CustomerTableId")
@Table(name="CUSTOMER", schema = "EX_IDTABLE")
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "customer_table_generator")
    @TableGenerator(name = "customer_table_generator", schema = "EX_IDTABLE", table = "ID_GENERATOR_TABLE")
    private Long id;

    private String name;
}
Figure 3: EmployeePK.java
Figure 4: An Employee with a composite Key/Id

3.5 UUIDs

  • Les UUIDs sont des identifiants universels uniques, générés aléatoirement.
  • Avantages: unicité globale, distribution, sans coordination centrale
  • Inconvénients: taille (16 octets), performance index, non séquentiel
    • Pour la pagination ajouter un index sur un attribut date de création
  • Usage: microservices, systèmes distribués, réplication multi-maîtres
@Getter
@Setter
@ToString
@RequiredArgsConstructor(staticName = "of")
@NoArgsConstructor(access = AccessLevel.PROTECTED)

@Entity
@Table(name = "CUSTOMER", schema = "EX_UUID")
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID uuid;

    @Column(length = 50, nullable = false)
    @NonNull
    private String name;
}
Figure 5: EmployeePK.java
Figure 6: A Customer with a UUID

4 Les clés composites

  • Types de clés
    • Clé atomique
      • Natural (Clé naturelle) : attribut métier unique (email, SIRET)
      • Surrogate (Clé artificielle) : identifiant technique unique (Long, UUID)
    • Clé composite
      • Natural : combinaison d’attributs métier (année + département + matricule)
      • Surrogate : combinaison d’identifiants techniques (shardId + sequenceId)
  • JPA propose deux approches pour gérer les clés composites:
    • @EmbeddedId : encapsule les attributs dans une classe dédiée
    • @IdClass : distribue les attributs dans l’entité

4.1 Embedded Id

  • Utilisation de l’annotation @EmbeddedId
  • Encapsulation des attributs de la clé dans une classe dédiée
  • La classe embarquée doit être annotée @Embeddable
    • Doit implémenter Serializable
    • Nécessite equals() et hashCode()
Figure 7: An Employee with a composite Key/Id
@Getter
@Setter

@Entity(name="EmployeEmbeddedId")
@Table(name="EMPLOYE", schema = "EX_EMBEDDED_ID")
public class Employee {
    @EmbeddedId
    private EmployeePK id;
    private String name;
}
Figure 8: Employee.java
@EqualsAndHashCode
@Embeddable
@AllArgsConstructor
@NoArgsConstructor
public class EmployeePK implements Serializable {
    private String department;
    private int rankInDepartment;
}
Figure 9: EmployeePK.java

4.2 Id Class

  • Utilisation de l’annotation @IdClass dans l’entité.
  • Les attributs de la clé sont définis dans l’entité
  • Une classe séparée définit la structure de la clé
  • Les noms des attributs doivent correspondre
  • Plus simple pour les requêtes JPQL
Figure 10: An Employee with a composite Key/Id
@Setter
@Getter

@Entity(name="EmployeIdClass")
@Table(name="EMPLOYE", schema = "EX_IDCLASS")
@IdClass(EmployeePK.class)
public class Employee {
    @Id
    private String department;
    @Id
    private int rankInDepartment;
    private String name;
}
Figure 11: Employee.java
@EqualsAndHashCode
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class EmployeePK implements Serializable {
    @Id
    private String department;
    @Id
    private int rankInDepartment;
}
Figure 12: EmployeePK.java

Réutilisation