Problématique de l'identifiant auto-incrémenté
Lors de la persistance d'entités avec Spring Data JPA, un comportement inattendu survient fréquemment : même si une valeur d'identifiant (ID) est explicitement définie dans l'objet métier avant l'appel à la méthode save(), JPA l'ignore et laisse la base de données (comme MySQL) générer un identifiant auto-incrémenté. Cet article détaille comment configurer JPA pour insérer des enregistrmeents avec des identifiants spécifiques, tout en conservant la possibilité d'utiliser l'auto-incrémentation par défaut lorsque l'ID n'est pas fourni.
Configuration de l'environnement
Pour illustrer cette solution, nous utilisons une base de données MySQL. Voici la structure de la table utilisée pour les tests :
CREATE TABLE product_catalog (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
product_name VARCHAR(50) NOT NULL DEFAULT '',
unit_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_product_name (product_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
La configuration Spring Boot standard pour JPA et la source de données est appliquée dans le fichier application.yml :
spring:
datasource:
url: jdbc:mysql://localhost:3306/store_db?useUnicode=true&characterEncoding=UTF-8&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: secret
jpa:
database: MYSQL
hibernate:
ddl-auto: none
show-sql: true
properties:
hibernate:
format_sql: true
Analyse du comportement par défaut (IDENTITY)
Par défaut, les développeurs utilisent la stratégie GenerationType.IDENTITY pour s'appuyer sur l'auto-incrémantation de MySQL. Définissons l'entité correspondante :
@Entity
@Table(name = "product_catalog")
@Data
public class ProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@Column(name = "product_name")
private String productName;
@Column(name = "unit_price")
private BigDecimal unitPrice;
@Column(name = "active")
private Byte active;
}
Le dépôt Spring Data est standard :
public interface ProductRepository extends JpaRepository<ProductEntity, Integer> {
}
Si nous tentons d'insérer un produit en spécifiant manuellement l'ID :
public void insertWithManualId() {
ProductEntity product = new ProductEntity();
product.setId(50);
product.setProductName("Souris Ergonomique");
product.setUnitPrice(new BigDecimal("45.00"));
product.setActive((byte) 1);
ProductEntity saved = productRepository.save(product);
System.out.println("ID généré : " + saved.getId());
}
En observant les logs SQL de Hibernate, on constate que la requête INSERT n'inclut pas la colonne id. MySQL génère donc un nouvel identifiant auto-incrémenté, ignorant complètement la valeur 50. Si l'enregistrement avec l'ID 50 existe déjà, JPA exécutera une requête UPDATE au lieu d'une insertion.
Implémentation d'une stratégie de génération flexible
Pour résoudre ce problème, nous devons modifier la stratégie de génération d'identifiants. Hibernate propose plusieurs stratégies via GenerationType. Nous allons utiliser AUTO combinée à un générateur personnalisé.
Créons une nouvelle entité utilisant cette approche :
@Entity
@Table(name = "product_catalog")
@Data
public class FlexibleProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "flexible-id-gen")
@GenericGenerator(
name = "flexible-id-gen",
strategy = "com.example.jpa.generator.FlexibleIdGenerator"
)
@Column(name = "id")
private Integer id;
@Column(name = "product_name")
private String productName;
@Column(name = "unit_price")
private BigDecimal unitPrice;
@Column(name = "active")
private Byte active;
}
L'annotation @GenericGenerator pointe vers notre classe personnalisée FlexibleIdGenerator. Voici l'implémentation de ce générateur, qui hérite de IdentityGenerator pour conserver le comportement d'auto-incrémentation en cas d'absence d'ID :
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.IdentityGenerator;
import org.hibernate.HibernateException;
import java.io.Serializable;
public class FlexibleIdGenerator extends IdentityGenerator {
@Override
public Serializable generate(SharedSessionContractImplementor session, Object entity) throws HibernateException {
Serializable entityId = session.getEntityPersister(null, entity)
.getClassMetadata()
.getIdentifier(entity, session);
if (entityId != null && Integer.parseInt(entityId.toString()) > 0) {
return entityId;
}
return super.generate(session, entity);
}
}
La logique est simple : le générateur extrait l'identifiant de l'entité. Si un identifiant valide (supérieur à 0) est présent, il est retourné tel quel, forçant Hibernate à l'inclure dans la requête SQL. Sinon, il délègue la génération à la classe parente, qui utilisera l'auto-incrémentation de la base de données.
Validation et analyse des requêtes SQL
Testons cette nouvelle configuration avec trois scénarios distincts :
public void testFlexibleGeneration() {
// Scénario 1 : Insertion avec un ID spécifique (l'enregistrement n'existe pas)
FlexibleProductEntity product1 = new FlexibleProductEntity();
product1.setId(99);
product1.setProductName("Clavier Mécanique");
product1.setUnitPrice(new BigDecimal("129.99"));
product1.setActive((byte) 1);
productRepository.save(product1);
// Scénario 2 : Mise à jour avec le même ID spécifique
product1.setUnitPrice(new BigDecimal("119.99"));
productRepository.save(product1);
// Scénario 3 : Insertion sans ID (auto-incrémentation)
FlexibleProductEntity product2 = new FlexibleProductEntity();
product2.setProductName("Tapis de Souris");
product2.setUnitPrice(new BigDecimal("15.50"));
product2.setActive((byte) 1);
productRepository.save(product2);
}
L'analyse des logs Hibernate confirme le comportement attendu :
-- Scénario 1 : L'ID 99 est bien inclus dans l'INSERT
Hibernate: select flexiblep0_.id as id1_0_0_ ... from product_catalog flexiblep0_ where flexiblep0_.id=?
Hibernate: insert into product_catalog (active, unit_price, product_name, id) values (?, ?, ?, ?)
-- Scénario 2 : L'enregistrement existant, une mise à jour est effectuée
Hibernate: select flexiblep0_.id as id1_0_0_ ... from product_catalog flexiblep0_ where flexiblep0_.id=?
Hibernate: update product_catalog set active=?, unit_price=?, product_name=? where id=?
-- Scénario 3 : Aucun ID fourni, la colonne id est omise dans l'INSERT
Hibernate: insert into product_catalog (active, unit_price, product_name) values (?, ?, ?)
Cette approche permet de maîtriser totalement la création des identifiants tout en gardant la flexibilité de l'auto-incrémentation native de MySQL lorsque cela est nécessaire.