Avec les versions Spring 6 et Spring Boot 3, Java 17+ est devenu la version de base du framework. C’est donc le moment idéal pour commencer à utiliser des enregistrements Java compacts en tant qu’objets de transfert de données (DTO) pour divers appels de base de données et d’API.
Que vous préfériez lire ou regarder, passons en revue quelques approches pour utiliser les enregistrements Java en tant que DTO qui s’appliquent à Spring Boot 3 avec Hibernate 6 comme fournisseur de persistance.
Exemple de base de données
Suivez ces instructions si vous souhaitez installer l’exemple de base de données et expérimenter vous-même. Sinon, n’hésitez pas à sauter cette section :
1. Téléchargez le jeu de données de la base de données Chinook (magasin de musique) pour la syntaxe PostgreSQL.
2. Démarrez une instance de YugabyteDB, une base de données distribuée compatible PostgreSQL, dans Docker :
mkdir ~/yb_docker_data
docker network create custom-network
docker run -d --name yugabytedb_node1 --net custom-network \
-p 7001:7000 -p 9000:9000 -p 5433:5433 \
-v ~/yb_docker_data/node1:/home/yugabyte/yb_data --restart unless-stopped \
yugabytedb/yugabyte:latest \
bin/yugabyted start \
--base_dir=/home/yugabyte/yb_data --daemon=false
3. Créez le chinook
base de données dans YugabyteDB :
createdb -h 127.0.0.1 -p 5433 -U yugabyte -E UTF8 chinook
4. Chargez l’exemple d’ensemble de données :
psql -h 127.0.0.1 -p 5433 -U yugabyte -f Chinook_PostgreSql_utf8.sql -d chinook
Ensuite, créez un exemple d’application Spring Boot 3 :
1. Générez un modèle d’application en utilisant Spring Boot 3+ et Java 17+ avec Spring Data JPA comme dépendance.
2. Ajoutez le pilote PostgreSQL au pom.xml
déposer:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.5.4</version>
</dependency>
3. Fournissez les paramètres de connectivité YugabyteDB dans le application.properties
déposer:
spring.datasource.url = jdbc:postgresql://127.0.0.1:5433/chinook
spring.datasource.username = yugabyte
spring.datasource.password = yugabyte
Tout est prêt ! Maintenant, vous êtes prêt à suivre le reste du guide.
Modèle de données
La base de données Chinook est livrée avec de nombreuses relations, mais deux tables suffiront amplement pour montrer comment utiliser les enregistrements Java en tant que DTO.
Le premier tableau est Track
et ci-dessous une définition d’une classe d’entité JPA correspondante :
@Entity
public class Track {
@Id
private Integer trackId;
@Column(nullable = false)
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "album_id")
private Album album;
@Column(nullable = false)
private Integer mediaTypeId;
private Integer genreId;
private String composer;
@Column(nullable = false)
private Integer milliseconds;
private Integer bytes;
@Column(nullable = false)
private BigDecimal unitPrice;
// Getters and setters are omitted
}
Le deuxième tableau est Album
et a la classe d’entité suivante :
@Entity
public class Album {
@Id
private Integer albumId;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private Integer artistId;
// Getters and setters are omitted
}
En plus des classes d’entités, créez un enregistrement Java nommé TrackRecord
qui stocke des informations courtes mais descriptives sur la chanson :
public record TrackRecord(String name, String album, String composer) {}
Approche naïve
Imaginez que vous deviez implémenter un point de terminaison REST qui renvoie une courte description de la chanson. L’API doit fournir les noms des chansons et des albums, ainsi que le nom de l’auteur.
Le créé précédemment TrackRecord
classe peut contenir les informations requises. Alors, créons un enregistrement en utilisant l’approche naïve qui obtient les données via JPA Entity
Des classes:
1. Ajoutez le référentiel JPA suivant :
public interface TrackRepository extends JpaRepository<Track, Integer> {
}
2. Ajoutez la méthode de niveau de service de Spring Boot qui crée un TrackRecord
instance de la Track
classe d’entité. Ce dernier est récupéré via le TrackRepository
exemple:
@Transactional(readOnly = true)
public TrackRecord getTrackRecord(Integer trackId) {
Track track = repository.findById(trackId).get();
TrackRecord trackRecord = new TrackRecord(
track.getName(),
track.getAlbum().getTitle(),
track.getComposer());
return trackRecord;
}
La solution semble simple et compacte, mais elle est très inefficace car Hibernate doit d’abord instancier deux entités : Track
et Album
(voir le track.getAlbum().getTitle()
). Pour cela, il génère deux requêtes SQL qui sollicitent toutes les colonnes des tables correspondantes de la base de données :
Hibernate:
select
t1_0.track_id,
t1_0.album_id,
t1_0.bytes,
t1_0.composer,
t1_0.genre_id,
t1_0.media_type_id,
t1_0.milliseconds,
t1_0.name,
t1_0.unit_price
from
track t1_0
where
t1_0.track_id=?
Hibernate:
select
a1_0.album_id,
a1_0.artist_id,
a1_0.title
from
album a1_0
where
a1_0.album_id=?
Hibernate sélectionne 12 colonnes sur deux tables, mais TrackRecord
n’a besoin que de trois colonnes ! C’est un gaspillage de mémoire, de calcul et de ressources réseau, en particulier si vous utilisez des bases de données distribuées comme YugabyteDB qui disperse les données sur plusieurs nœuds de cluster.
TupleTransformer
L’approche naïve peut être facilement corrigée si vous interrogez uniquement les enregistrements requis par l’API, puis transformez un ensemble de résultats de requête en un enregistrement Java respectif.
Le module Spring Data de Spring Boot 3 s’appuie sur Hibernate 6. Cette version d’Hibernate divise le ResultTransformer
interface en deux interfaces : TupleTransformer
et ResultListTransformer
.
Le TupleTransformer
classe prend en charge les enregistrements Java, donc, l’implémentation du public TrackRecord getTrackRecord(Integer trackId)
peut être optimisé de cette façon :
@Transactional(readOnly = true)
public TrackRecord getTrackRecord(Integer trackId) {
org.hibernate.query.Query<TrackRecord> query = entityManager.createQuery(
"""
SELECT t.name, a.title, t.composer
FROM Track t
JOIN Album a ON t.album.albumId=a.albumId
WHERE t.trackId=:id
""").
setParameter("id", trackId).
unwrap(org.hibernate.query.Query.class);
TrackRecord trackRecord = query.setTupleTransformer((tuple, aliases) -> {
return new TrackRecord(
(String) tuple[0],
(String) tuple[1],
(String) tuple[2]);
}).getSingleResult();
return trackRecord;
}
entityManager.createQuery(...)
– Crée une requête JPA qui demande trois colonnes nécessaires pour leTrackRecord
classe.query.setTupleTransformer(...)
– LeTupleTransformer
prend en charge les enregistrements Java, ce qui signifie qu’unTrackRecord
instance peut être créée dans l’implémentation du transformateur.
Cette approche est plus efficace que la précédente car vous n’avez plus besoin de créer des classes d’entités et pouvez facilement construire un enregistrement Java avec le TupleTransformer
. De plus, Hibernate génère une seule requête SQL qui renvoie uniquement les colonnes requises :
Hibernate:
select
t1_0.name,
a1_0.title,
t1_0.composer
from
track t1_0
join
album a1_0
on t1_0.album_id=a1_0.album_id
where
t1_0.track_id=?
Cependant, il y a un inconvénient très visible à cette approche : la mise en œuvre de la public TrackRecord getTrackRecord(Integer trackId)
la méthode est devenue plus longue et plus verbeuse.
Enregistrement Java dans la requête JPA
Il existe plusieurs façons de raccourcir l’implémentation précédente. L’une consiste à instancier une instance d’enregistrement Java dans une requête JPA.
Tout d’abord, étendre la mise en œuvre de la TrackRepository
interface avec une requête personnalisée qui crée un TrackRecord
instance à partir des colonnes de base de données demandées :
public interface TrackRepository extends JpaRepository<Track, Integer> {
@Query("""
SELECT new com.my.springboot.app.TrackRecord(t.name, a.title, t.composer)
FROM Track t
JOIN Album a ON t.album.albumId=a.albumId
WHERE t.trackId=:id
""")
TrackRecord findTrackRecord(@Param("id") Integer trackId);
}
Ensuite, mettez à jour la mise en œuvre du TrackRecord getTrackRecord(Integer trackId)
méthode de cette façon :
@Transactional(readOnly = true)
public TrackRecord getTrackRecord(Integer trackId) {
return repository.findTrackRecord(trackId);
}
Ainsi, la mise en œuvre de la méthode est devenue un one-liner qui obtient un TrackRecord
instance directement depuis le référentiel JPA : le plus simple possible.
Mais ce n’est pas tout. Il y a encore un petit problème. La requête JPA qui construit un enregistrement Java nécessite que vous fournissiez un nom de package complet pour le TrackRecord
classe:
SELECT new com.my.springboot.app.TrackRecord(t.name, a.title, t.composer)...
Trouvons un moyen de contourner cette exigence. Idéalement, l’enregistrement Java doit être instancié sans le nom du package :
SELECT new TrackRecord(t.name, a.title, t.composer)...
Utilitaires d’hypersistence
La bibliothèque Hypersistence Utils est livrée avec de nombreux avantages pour Spring et Hibernate. Une fonctionnalité vous permet de créer une instance d’enregistrement Java dans une requête JPA sans le nom du package.
Activons la bibliothèque et cette fonctionnalité liée aux enregistrements Java dans l’application Spring Boot :
1. Ajoutez l’artefact Maven de la bibliothèque pour Hibernate 6.
2. Créez une coutume IntegratorProvider
qui enregistre TrackRecord
classe avec Hibernate :
public class ClassImportIntegratorProvider implements IntegratorProvider {
@Override
public List<Integrator> getIntegrators() {
return List.of(new ClassImportIntegrator(List.of(TrackRecord.class)));
}
}
3. Mettez à jour le application.properties
fichier en ajoutant cette coutume IntegratorProvider
: