Une question récente et vraiment cool sur Stack Overflow était de savoir comment mapper une collection imbriquée dans un Java Map
avec jOOQ. Dans le passé, j’ai blogué sur le puissant MULTISET
opérateur plusieurs fois, ce qui permet d’imbriquer des collections dans jOOQ. Cette fois, au lieu d’imbriquer les données dans un List<UserType>
pourquoi ne pas l’emboîter dans un Map<UserType1, UserType2>
plutôt?
En regardant la base de données Sakila, regardons comment nous pouvons récupérer des données dans ce Java record
taper:
record Film(
String title,
Map<LocalDate, BigDecimal> revenue
) {}
Le type de résultat doit conclure le FILM.TITLE
ainsi que le montant d’argent ou de location de DVD réalisé chaque jour, par film. Nous pourrions utiliser d’autres structures de données, mais supposons que c’est ce que nous voulons consommer dans l’interface utilisateur ou le service ou quoi que ce soit.
Dans jOOQ, comme toujours, grâce à MULTISET
nous pouvons le faire en une seule requête qui ressemble à ceci :
List<Film> result =
ctx.select(
FILM.TITLE,
multiset(
select(
PAYMENT.PAYMENT_DATE.cast(LOCALDATE),
sum(PAYMENT.AMOUNT))
.from(PAYMENT)
.where(PAYMENT.rental().inventory().FILM_ID
.eq(FILM.FILM_ID))
.groupBy(PAYMENT.PAYMENT_DATE.cast(LOCALDATE))
.orderBy(PAYMENT.PAYMENT_DATE.cast(LOCALDATE))
)
// Convert Field<Result<Record2<LocalDate, BigDecimal>>>
// to Field<Map<LocalDate, BigDecimal>>
.convertFrom(r -> r.collect(Records.intoMap())
)
.from(FILM)
.orderBy(FILM.TITLE)
// Convert Record2<String, Map<LocalDate, BigDecimal>>
// to List<Film>
.fetch(Records.mapping(Film::new))
Nous pouvons ensuite consommer le résultat, par exemple comme ceci :
for (Film film : result) {
System.out.println();
System.out.println("Film %s with revenue: "
.formatted(film.title()));
// Inferred types are LocalDate d and BigDecimal r
film.revenue().forEach((d, r) ->
System.out.println(" %s: %s".formatted(d, r))
);
}
Produire:
Film ACADEMY DINOSAUR with revenue: 2005-05-27: 0.99 2005-05-30: 1.99 2005-06-15: 0.99 [...] Film ACE GOLDFINGER with revenue: 2005-07-07: 4.99 2005-07-28: 9.99 2005-08-01: 4.99 [...]
Tout est, comme toujours avec jOOQ, complètement sécurisé ! Essayez vous-même, modifiez certaines des expressions de colonne dans la requête ou le résultat record
ou Map
tapez pour voir que la requête arrête de compiler !
La partie intéressante ici est:
.convertFrom(r -> r.collect(Records.intoMap())
Le Field.convertFrom()
La méthode provient de la nouvelle API de conversion ad hoc de jOOQ 3.15, qui permet la conversion ad hoc d’un Field<T>
expression de colonne à un Field<U>
expression de colonne. Dans ce cas, la conversion va:
- Depuis
Field<Result<Record2<LocalDate, BigDecimal>>>
(le type de champ multiset) - Pour
Field<Map<LocalDate, BigDecimal>>
(le type mappé)
Il le fait en collectant tous les Record2<LocalDate, BigDecimal>
enregistrements de la collection imbriquée dans un Map<LocalDate, BigDecimal>
en utilisant le Records.intoMap()
collectionneur. La signature de cette méthode est :
public static final <K, V, R entends Record2<K, V>>
Collector<R, ?, Map<K, V>> intoMap() { ... }
Cet usage spécifique des génériques permet d’éviter la répétition des expressions clé et valeur du champ, sachant qu’une collection de Record2<K, V>
a un moyen évident de se rassembler dans un Map<K, V>
(ou Map<K, List<V>>
si vous utilisez Records.intoGroups()
si les clés peuvent être dupliquées).
Notez que ces deux collecteurs produiront un ordre d’insertion préservant Map
(par exemple LinkedHashMap
), de sorte que tout MULTISET
la commande sera préservée.
Conclusion
Le ciel est la limite, lorsque vous utilisez les nouvelles capacités d’imbrication de jOOQ 3.15 pour les collections imbriquées (MULTISET
ou MULTISET_AGG
) ou des enregistrements imbriqués (ROW
). Avec des convertisseurs ad hoc, vous pouvez mapper la représentation jOOQ dans n’importe quelle représentation Java au milieu de votre requête, pour adhérer à n’importe quel type de cible de votre choix, y compris imbriqué Map<K, V>
avec des types arbitraires de K
et V