Présentation des identifiants dans Hibernate / JPA

1. Introduction

Les identificateurs dans Hibernate représentent la clé primaire d'une entité. Cela implique que les valeurs sont uniques afin qu'elles puissent identifier une entité spécifique, qu'elles ne sont pas nulles et qu'elles ne seront pas modifiées.

Hibernate propose différentes manières de définir des identifiants. Dans cet article, nous examinerons chaque méthode de mappage des identifiants d'entité à l'aide de la bibliothèque.

2. Identificateurs simples

Le moyen le plus simple de définir un identifiant consiste à utiliser l' annotation @Id .

Les identifiants simples sont mappés à l'aide de @Id à une propriété unique de l'un de ces types: types de wrapper Java primitif et primitif, String, Date, BigDecimal, BigInteger.

Voyons un exemple rapide de définition d'une entité avec une clé primaire de type long:

@Entity public class Student { @Id private long studentId; // standard constructor, getters, setters }

3. Identifiants générés

Si nous voulons que la valeur de la clé primaire soit générée automatiquement pour nous, nous pouvons ajouter l' annotation @GeneratedValue .

Cela peut utiliser 4 types de génération: AUTO, IDENTITY, SEQUENCE, TABLE.

Si nous ne spécifions pas de valeur explicitement, le type de génération par défaut est AUTO.

3.1. Génération AUTO

Si nous utilisons le type de génération par défaut, le fournisseur de persistance déterminera les valeurs en fonction du type de l'attribut de clé primaire. Ce type peut être numérique ou UUID.

Pour les valeurs numériques, la génération est basée sur un générateur de séquence ou de table, tandis que les valeurs UUID utiliseront UUIDGenerator.

Voyons un exemple de mappage d'une clé primaire d'entité à l'aide de la stratégie de génération AUTO:

@Entity public class Student { @Id @GeneratedValue private long studentId; // ... }

Dans ce cas, les valeurs de clé primaire seront uniques au niveau de la base de données.

Une fonctionnalité intéressante introduite dans Hibernate 5 est l' UUIDGenerator. Pour l'utiliser, il suffit de déclarer un identifiant de type UUID avec l' annotation @GeneratedValue :

@Entity public class Course { @Id @GeneratedValue private UUID courseId; // ... }

Hibernate générera un identifiant de la forme «8dd5f315-9788-4d00-87bb-10eed9eff566».

3.2. Génération IDENTITY

Ce type de génération repose sur IdentityGenerator qui attend des valeurs générées par une colonne d' identité dans la base de données, ce qui signifie qu'elles sont auto-incrémentées.

Pour utiliser ce type de génération, il suffit de définir le paramètre de stratégie :

@Entity public class Student { @Id @GeneratedValue (strategy = GenerationType.IDENTITY) private long studentId; // ... }

Une chose à noter est que la génération d'IDENTITY désactive les mises à jour par lots.

3.3. Génération SEQUENCE

Pour utiliser un identifiant basé sur une séquence, Hibernate fournit la classe SequenceStyleGenerator .

Ce générateur utilise des séquences si elles sont prises en charge par notre base de données et passe à la génération de table si elles ne le sont pas.

Pour personnaliser le nom de la séquence, nous pouvons utiliser l' annotation @GenericGenerator avec la stratégie SequenceStyleGenerator:

@Entity public class User { @Id @GeneratedValue(generator = "sequence-generator") @GenericGenerator( name = "sequence-generator", strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator", parameters = { @Parameter(name = "sequence_name", value = "user_sequence"), @Parameter(name = "initial_value", value = "4"), @Parameter(name = "increment_size", value = "1") } ) private long userId; // ... }

Dans cet exemple, nous avons également défini une valeur initiale pour la séquence, ce qui signifie que la génération de clé primaire commencera à 4.

SEQUENCE est le type de génération recommandé par la documentation Hibernate.

Les valeurs générées sont uniques par séquence. Si vous ne spécifiez pas de nom de séquence, Hibernate réutilisera la même séquence hibernate_sequence pour différents types.

3.4. Génération de TABLE

Le TableGenerator utilise une table de base de données sous-jacente qui contient des segments de valeurs de génération d'identifiant.

Personnalisons le nom de la table à l'aide de l' annotation @TableGenerator :

@Entity public class Department { @Id @GeneratedValue(strategy = GenerationType.TABLE, generator = "table-generator") @TableGenerator(name = "table-generator", table = "dep_ids", pkColumnName = "seq_id", valueColumnName = "seq_value") private long depId; // ... }

Dans cet exemple, nous pouvons voir que d'autres attributs tels que pkColumnName et valueColumnName peuvent également être personnalisés.

L'inconvénient de cette méthode est qu'elle ne s'adapte pas bien et peut affecter négativement les performances.

Pour résumer, ces quatre types de génération entraîneront la génération de valeurs similaires mais utiliseront des mécanismes de base de données différents.

3.5. Générateur personnalisé

Si nous ne voulons utiliser aucune des stratégies prêtes à l'emploi, nous pouvons définir notre générateur personnalisé en implémentant l' interface IdentifierGenerator .

Créons un générateur qui construit des identifiants contenant un préfixe String et un nombre:

public class MyGenerator implements IdentifierGenerator, Configurable { private String prefix; @Override public Serializable generate( SharedSessionContractImplementor session, Object obj) throws HibernateException { String query = String.format("select %s from %s", session.getEntityPersister(obj.getClass().getName(), obj) .getIdentifierPropertyName(), obj.getClass().getSimpleName()); Stream ids = session.createQuery(query).stream(); Long max = ids.map(o -> o.replace(prefix + "-", "")) .mapToLong(Long::parseLong) .max() .orElse(0L); return prefix + "-" + (max + 1); } @Override public void configure(Type type, Properties properties, ServiceRegistry serviceRegistry) throws MappingException { prefix = properties.getProperty("prefix"); } }

In this example, we override the generate() method from the IdentifierGenerator interface and first find the highest number from the existing primary keys of the form prefix-XX.

Then we add 1 to the maximum number found and append the prefix property to obtain the newly generated id value.

Our class also implements the Configurable interface, so that we can set the prefix property value in the configure() method.

Next, let's add this custom generator to an entity. For this, we can use the @GenericGenerator annotation with a strategy parameter that contains the full class name of our generator class:

@Entity public class Product { @Id @GeneratedValue(generator = "prod-generator") @GenericGenerator(name = "prod-generator", parameters = @Parameter(name = "prefix", value = "prod"), strategy = "com.baeldung.hibernate.pojo.generator.MyGenerator") private String prodId; // ... }

Also, notice we've set the prefix parameter to “prod”.

Let's see a quick JUnit test for a clearer understanding of the id values generated:

@Test public void whenSaveCustomGeneratedId_thenOk() { Product product = new Product(); session.save(product); Product product2 = new Product(); session.save(product2); assertThat(product2.getProdId()).isEqualTo("prod-2"); }

Here, the first value generated using the “prod” prefix was “prod-1”, followed by “prod-2”.

4. Composite Identifiers

Besides the simple identifiers we've seen so far, Hibernate also allows us to define composite identifiers.

A composite id is represented by a primary key class with one or more persistent attributes.

The primary key class must fulfill several conditions:

  • it should be defined using @EmbeddedId or @IdClass annotations
  • it should be public, serializable and have a public no-arg constructor
  • it should implement equals() and hashCode() methods

The class's attributes can be basic, composite or ManyToOne while avoiding collections and OneToOne attributes.

4.1. @EmbeddedId

To define an id using @EmbeddedId, first we need a primary key class annotated with @Embeddable:

@Embeddable public class OrderEntryPK implements Serializable { private long orderId; private long productId; // standard constructor, getters, setters // equals() and hashCode() }

Next, we can add an id of type OrderEntryPK to an entity using @EmbeddedId:

@Entity public class OrderEntry { @EmbeddedId private OrderEntryPK entryId; // ... }

Let's see how we can use this type of composite id to set the primary key for an entity:

@Test public void whenSaveCompositeIdEntity_thenOk() { OrderEntryPK entryPK = new OrderEntryPK(); entryPK.setOrderId(1L); entryPK.setProductId(30L); OrderEntry entry = new OrderEntry(); entry.setEntryId(entryPK); session.save(entry); assertThat(entry.getEntryId().getOrderId()).isEqualTo(1L); }

Here the OrderEntry object has an OrderEntryPK primary id formed of two attributes: orderId and productId.

4.2. @IdClass

The @IdClass annotation is similar to the @EmbeddedId, except the attributes are defined in the main entity class using @Id for each one.

The primary-key class will look the same as before.

Let's rewrite the OrderEntry example with an @IdClass:

@Entity @IdClass(OrderEntryPK.class) public class OrderEntry { @Id private long orderId; @Id private long productId; // ... }

Then we can set the id values directly on the OrderEntry object:

@Test public void whenSaveIdClassEntity_thenOk() { OrderEntry entry = new OrderEntry(); entry.setOrderId(1L); entry.setProductId(30L); session.save(entry); assertThat(entry.getOrderId()).isEqualTo(1L); }

Note that for both types of composite ids, the primary key class can also contain @ManyToOne attributes.

Hibernate also allows defining primary-keys made up of @ManyToOne associations combined with @Id annotation. In this case, the entity class should also fulfill the conditions of a primary-key class.

The disadvantage of this method is that there's no separation between the entity object and the identifier.

5. Derived Identifiers

Derived identifiers are obtained from an entity's association using the @MapsId annotation.

First, let's create a UserProfile entity which derives its id from a one-to-one association with the User entity:

@Entity public class UserProfile { @Id private long profileId; @OneToOne @MapsId private User user; // ... }

Next, let's verify that a UserProfile instance has the same id as its associated User instance:

@Test public void whenSaveDerivedIdEntity_thenOk() { User user = new User(); session.save(user); UserProfile profile = new UserProfile(); profile.setUser(user); session.save(profile); assertThat(profile.getProfileId()).isEqualTo(user.getUserId()); }

6. Conclusion

Dans cet article, nous avons vu les multiples façons dont nous pouvons définir des identifiants dans Hibernate.

Le code source complet des exemples est disponible à l'adresse over sur GitHub.