Hibernate-reactive: récupération de collection paresseuse dans Quarkus

Créé le 12 mars 2021  ·  30Commentaires  ·  Source: hibernate/hibernate-reactive

Lorsque vous essayez de récupérer une association paresseusement avec Mutiny dans Quarkus, cela ne fonctionne pas si la récupération est effectuée dans une session différente de celle utilisée pour conserver les données. Si la même session est utilisée (donc récupérer juste après avoir persisté dans la même session), cela fonctionne.

Comportement attendu
Il fonctionne également dans différentes sessions.

Comportement réel
Une exception est levée lors de la tentative de récupération de l'association :

2021-03-12 09:56:23,634 ERROR [org.jbo.res.rea.com.cor.AbstractResteasyReactiveContext] (vert.x-eventloop-thread-2) Request failed: org.hibernate.LazyInitializationException: Collection cannot be initialized: com.example.Author.books
    at org.hibernate.reactive.session.impl.ReactiveSessionImpl.initializeCollection(ReactiveSessionImpl.java:330)
    at org.hibernate.collection.internal.AbstractPersistentCollection$4.doWork(AbstractPersistentCollection.java:589)
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:264)
    at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:585)
    at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:149)
    at org.hibernate.collection.internal.AbstractPersistentCollection$1.doWork(AbstractPersistentCollection.java:178)
    at org.hibernate.collection.internal.AbstractPersistentCollection$1.doWork(AbstractPersistentCollection.java:163)
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:264)
    at org.hibernate.collection.internal.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:162)
    at org.hibernate.collection.internal.PersistentBag.size(PersistentBag.java:371)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.takeCollectionSizeSnapshot(LazyAttributeLoadingInterceptor.java:160)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.lambda$loadAttribute$0(LazyAttributeLoadingInterceptor.java:110)
    at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper.performWork(EnhancementHelper.java:130)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.loadAttribute(LazyAttributeLoadingInterceptor.java:76)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.fetchAttribute(LazyAttributeLoadingInterceptor.java:72)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.handleRead(LazyAttributeLoadingInterceptor.java:53)
    at org.hibernate.bytecode.enhance.spi.interceptor.AbstractInterceptor.readObject(AbstractInterceptor.java:153)
    at com.example.Author.$$_hibernate_read_books(Author.java)
    at com.example.Author.getBooks(Author.java:43)
    at com.example.TestResource.lambda$getBooks$2(TestResource.java:61)
    at io.smallrye.mutiny.operators.UniOnItemTransformToUni.invokeAndSubstitute(UniOnItemTransformToUni.java:31)
    at io.smallrye.mutiny.operators.UniOnItemTransformToUni$2.onItem(UniOnItemTransformToUni.java:74)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.lambda$onItem$1(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.context.impl.wrappers.SlowContextualExecutor.execute(SlowContextualExecutor.java:19)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.onItem(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.mutiny.operators.UniSerializedSubscriber.onItem(UniSerializedSubscriber.java:85)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.lambda$onItem$1(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.context.impl.wrappers.SlowContextualExecutor.execute(SlowContextualExecutor.java:19)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.onItem(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.mutiny.operators.UniDelegatingSubscriber.onItem(UniDelegatingSubscriber.java:24)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.lambda$onItem$1(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.context.impl.wrappers.SlowContextualExecutor.execute(SlowContextualExecutor.java:19)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.onItem(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.mutiny.operators.UniSerializedSubscriber.onItem(UniSerializedSubscriber.java:85)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.lambda$onItem$1(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.context.impl.wrappers.SlowContextualExecutor.execute(SlowContextualExecutor.java:19)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.onItem(ContextPropagationUniInterceptor.java:31)
    at io.smallrye.mutiny.operators.uni.builders.UniCreateFromCompletionStage.lambda$forwardFromCompletionStage$1(UniCreateFromCompletionStage.java:30)
    at java.base/java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:859)
    at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:837)
    at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506)
    at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2073)
    at com.ibm.asyncutil.iteration.AsyncTrampoline$TrampolineInternal.unroll(AsyncTrampoline.java:127)
    at com.ibm.asyncutil.iteration.AsyncTrampoline$TrampolineInternal.lambda$unroll$0(AsyncTrampoline.java:123)
    at java.base/java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:859)
    at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:837)
    at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506)
    at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2073)
    at org.hibernate.reactive.pool.impl.Handlers.lambda$toCompletionStage$0(Handlers.java:26)
    at io.vertx.sqlclient.impl.SqlResultHandler.complete(SqlResultHandler.java:98)
    at io.vertx.sqlclient.impl.SqlResultHandler.handle(SqlResultHandler.java:87)
    at io.vertx.sqlclient.impl.SqlResultHandler.handle(SqlResultHandler.java:33)
    at io.vertx.sqlclient.impl.SocketConnectionBase.handleMessage(SocketConnectionBase.java:241)
    at io.vertx.sqlclient.impl.SocketConnectionBase.lambda$init$0(SocketConnectionBase.java:88)
    at io.vertx.core.net.impl.NetSocketImpl.lambda$new$2(NetSocketImpl.java:101)
    at io.vertx.core.streams.impl.InboundBuffer.handleEvent(InboundBuffer.java:237)
    at io.vertx.core.streams.impl.InboundBuffer.write(InboundBuffer.java:127)
    at io.vertx.core.net.impl.NetSocketImpl.handleMessage(NetSocketImpl.java:357)
    at io.vertx.core.impl.ContextImpl.executeTask(ContextImpl.java:366)
    at io.vertx.core.impl.EventLoopContext.execute(EventLoopContext.java:43)
    at io.vertx.core.impl.ContextImpl.executeFromIO(ContextImpl.java:229)
    at io.vertx.core.net.impl.VertxHandler.channelRead(VertxHandler.java:163)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
    at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
    at io.vertx.pgclient.impl.codec.PgEncoder.lambda$write$0(PgEncoder.java:78)
    at io.vertx.pgclient.impl.codec.PgCommandCodec.handleReadyForQuery(PgCommandCodec.java:138)
    at io.vertx.pgclient.impl.codec.PgDecoder.decodeReadyForQuery(PgDecoder.java:226)
    at io.vertx.pgclient.impl.codec.PgDecoder.channelRead(PgDecoder.java:86)
    at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)

Reproducteur

https://github.com/markusdlugi/hibernate-reactive-lazy-fetching

Étapes pour reproduire le comportement :

  1. Démarrez la base de données en utilisant infrastructure/docker-compose.yml .
  2. Démarrez l'application Quarkus en utilisant mvn quarkus:dev
  3. Envoyer la demande : POST http://localhost:8080/test/working .
    3.1. Cela fonctionne et renvoie une liste de livres.
  4. Envoyer la demande : GET http://localhost:8080/test/failing/1 (ou autre authorId créé via POST http://localhost:8080/test/failing ).
    4.1 Cela échoue avec l'exception ci-dessus.
  • Version Quarkus
  • Hibernate Reactive Version : 1.0.0.Beta4 (également testé avec 1.0.0.CR1 en écrasant la version dans Maven, même comportement)
  • Hibernate Core Version : 5.4.28.Final (également testé avec 5.4.29.Final en écrasant la version dans Maven, même comportement)
bug

Commentaire le plus utile

Je viens de le tester et je peux confirmer que la récupération de collection paresseuse fonctionne désormais dans Quarkus. Merci encore :sourire:

Tous les 30 commentaires

/cc @DavidD @gavinking

Oui, je pense que nous avons décidé que c'était la bonne chose à faire : dans JPA, il n'y a aucune notion de réassociation d'une instance d'entité à une session. C'est un peu différent de l'ancien modèle d'utilisation d'Hibernate où vous pouviez utiliser saveOrUpdate() pour réassocier directement un objet détaché. Dans JPA, le modèle de gestion des objets détachés est l'opération merge() .

(Et il y a de bonnes raisons de ne pas l'avoir. L'expérience est que dans la pratique, les utilisateurs se retrouvent dans le pétrin avec la réassociation.)

Étant donné que le chargement d'une collection détachée implique vraiment de rattacher son parent, nous ne le prenons tout simplement pas en charge.

Cependant, IIRC, je vous laisse faire cela dans un StatelessSession , car c'est beaucoup plus compatible avec ce modèle de programmation. Essayez d'utiliser une session sans état et dites-moi si cela fonctionne pour vous.

Salut @gavinking , merci pour la réponse rapide.

Je ne suis pas sûr d'avoir compris ce que vous dites. Donc ce que j'ai implémenté était exactement ce qui est décrit dans la documentation :

session.find(Author.class, authorId)
    .chain(author -> Mutiny.fetch(author.getBooks()));

Cela fonctionne si l'auteur et ses livres ont été conservés avec la même session (c'est-à-dire dans la même requête HTTP), mais cela ne fonctionne pas si j'utilise une autre session (c'est-à-dire une autre requête).

Vous sous-entendez donc qu'il s'agit d'un comportement intentionnel, au moins pour une session « normale » (avec état ?) ? Cela me semble être un cas d'utilisation vraiment standard, car j'essaierai presque toujours d'obtenir des données de la base de données quelque temps après les avoir conservées, donc dans une session différente. Cela signifierait qu'en utilisant une session normale, les associations paresseuses ne sont pas prises en charge. Dans ce cas, je dirais qu'il devrait être explicitement mentionné dans la documentation qu'un StatelessSession doit être utilisé si des associations paresseuses doivent être récupérées.

Je dis que dans la nouvelle session, vous devez utiliser getReference() pour obtenir une référence liée à la session à l'entité qui possède la collection. Ensuite, vous pouvez récupérer la collection comme vous le souhaitez.

C'est le modèle JPA standard, c'est même un peu plus simple à utiliser en RH car j'ai surchargé getReference() .

@gavinking voulez-vous dire quelque chose comme ça?

Author reference = session.getReference( Author.class, authorId );
return Mutiny.fetch( reference.getBooks() );

Bien sûr, mais je l'ai rendu encore plus facile. Vous pouvez directement passer l'auteur détaché à getReference() .

D'accord, j'ai essayé ces suggestions, mais malheureusement elles n'ont pas fonctionné non plus. Peut-être pourriez-vous avoir la gentillesse de signaler si je fais quelque chose de mal ici :

https://github.com/markusdlugi/hibernate-reactive-lazy-fetching/blob/f3458eee969d7c3597560aaf8a646b7aad2ee9b0/src/main/java/com/example/TestResource.java#L55 -L79

L'approche utilisant getReference() échoue avec l'exception suivante :

2021-03-12 14:17:51,381 ERROR [org.jbo.res.rea.ser.cor.ExceptionMapping] (vert.x-eventloop-thread-10) Request failed : java.lang.NullPointerException
    at org.hibernate.engine.internal.AbstractEntityEntry.overwriteLoadedStateCollectionValue(AbstractEntityEntry.java:336)
    at org.hibernate.persister.entity.AbstractEntityPersister.initializeLazyProperty(AbstractEntityPersister.java:1144)
    at org.hibernate.persister.entity.AbstractEntityPersister.initializeEnhancedEntityUsedAsProxy(AbstractEntityPersister.java:4497)
    at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor.forceInitialize(EnhancementAsProxyLazinessInterceptor.java:221)
    at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor.lambda$handleRead$0(EnhancementAsProxyLazinessInterceptor.java:133)
    at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper.performWork(EnhancementHelper.java:130)
    at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor.handleRead(EnhancementAsProxyLazinessInterceptor.java:98)
    at org.hibernate.bytecode.enhance.spi.interceptor.AbstractInterceptor.readObject(AbstractInterceptor.java:153)
    at com.example.Author.$$_hibernate_read_books(Author.java)
    at com.example.Author.getBooks(Author.java:43)
    at com.example.TestResource.getBooksUsingReference(TestResource.java:69)
    ...

L'autre solution suggérée utilisant StatelessSession échoue avec :

2021-03-12 14:21:11,840 ERROR [org.jbo.res.rea.ser.cor.ExceptionMapping] (vert.x-eventloop-thread-5) Request failed : org.hibernate.LazyInitializationException: Unable to perform requested lazy initialization [com.example.Author.books] - no session and settings disallow loading outside the Session
    at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper.throwLazyInitializationException(EnhancementHelper.java:199)
    at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper.performWork(EnhancementHelper.java:89)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.loadAttribute(LazyAttributeLoadingInterceptor.java:76)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.fetchAttribute(LazyAttributeLoadingInterceptor.java:72)
    at org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor.handleRead(LazyAttributeLoadingInterceptor.java:53)
    at org.hibernate.bytecode.enhance.spi.interceptor.AbstractInterceptor.readObject(AbstractInterceptor.java:153)
    at com.example.Author.$$_hibernate_read_books(Author.java)
    at com.example.Author.getBooks(Author.java:43)
    at com.example.TestResource.lambda$getBooksUsingStatelessSession$3(TestResource.java:78)
    ...

Je vais jeter un œil, cela semble lié aux améliorations du bytecode

@DavideD , une mise à jour ?

J'ai essayé de reproduire le problème sans Quarkus en utilisant simplement le session-example dans ce référentiel, mais je n'ai pas pu le faire. Avec juste Hibernate Reactive et simple Mutiny, cela fonctionne comme prévu. Cela fonctionne également en utilisant find() , aucun getReference() ou StatelessSession nécessaire.

Ce problème ne semble donc se produire que dans Quarkus.

Je suis toujours en train d'y réfléchir.

Le problème se produit en raison des améliorations du bytecode qui sont activées par défaut dans Quarkus.
Je n'ai toujours pas compris quel ensemble de propriétés recréent le problème en utilisant uniquement Hibernate Reactive. Mais je vais continuer à travailler dessus aujourd'hui

Je peux maintenant recréer l'erreur en réactif. Cela nécessite deux choses :

  1. définir hibernate.bytecode.allow_enhancement_as_proxy sur vrai
  2. définissez SessionFactoryOptions#enableCollectionInDefaultFetchGroup sur faux. Il doit être défini sur true : https://github.com/hibernate/hibernate-reactive/blob/a3f4c61eef71baab8ca321da5af0bfb9052bde4b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/ReactiveSesessionServicesionFacva #L27

@gavinking Ai-je raison de penser que nous devrions nous assurer que dans Quarkus enableCollectionInDefaultFetchGroup est défini sur false ? J'avoue qu'en ce moment je ne comprends pas tout le problème (j'ai essayé de suivre la conversation sur le numéro 374)

Euh. Je pense qu'il est logique d'avoir la collection dans le groupe d'extraction par défaut. Je ne pense pas qu'il soit logique de l'exclure.

n Quarkus enableCollectionInDefaultFetchGroup est défini sur false

Désolé, oui, je voulais écrire is set to true . Temps pour une pause :-)

Ah OK bien :-) Faites une pause :-)

@DavideD Ce sont d'excellentes découvertes, merci :)

Je voulais juste souligner que d'après la discussion dans #374 et le hibernate/hibernate-orm#3558 associé, il semble que enableCollectionInDefaultFetchGroup été délibérément laissé à false dans Quarkus ? C'est du moins ce que je retiens de ce commentaire :

https://github.com/hibernate/hibernate-orm/pull/3558#issuecomment -695003875

Quoi qu'il en soit, il semble que Quarkus ne définisse actuellement pas cette propriété sur true , car elle n'est pas configurée dans FastBootReactiveEntityManagerFactoryBuilder :

https://github.com/quarkusio/quarkus/blob/55cf15173ff7b2985a8422de050d1c9c708be57c/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/FastBootReactiveEntityLvatory.jaFac

Et si je l'ai vérifié correctement, la valeur par défaut dans Hibernate ORM semble toujours être false , ce qui expliquerait pourquoi nous ne voyons ce comportement que dans Quarkus.

@Sanne doit regarder celui-ci.

Si nous devons seulement le définir sur true, cela semble assez facile à résoudre. Je vais créer un correctif, puis vérifier auprès de @Sanne

Eh bien, ce que je me demande, c'est s'il est défini sur false parce que c'est ce dont ORM a besoin, et si nous devons faire attention à ne pas marcher sur Hibernate régulier?

bonne trouvaille, merci. Oui c'est un peu délicat car les deux extensions ("regular" et "reactive") sont des ATM fortement couplés.

Besoin de les séparer correctement, cela a été développé dans trop de précipitation.

(OTOH, je ne sais pas pourquoi ORM en aurait besoin ; comme je l'ai déjà dit, je pense que cela devrait être activé par défaut.)

Hum ok. Faisons cela pour les deux ORM (dans Quarkus).

@gavinking , je pense que ce titre est faux, n'est-ce pas ? Je suppose que vous vouliez y mettre enableCollectionInDefaultFetchGroup place. Les améliorations du bytecode sont de toute façon nécessaires pour Quarkus à cause du natif, je suppose :)

Ah oui, bien sûr, je suppose que tu as raison.

vous dites que vous voyez ce bogue avec les paramètres par défaut dans Quarkus ?

Oui, aucune configuration spéciale n'est nécessaire dans Quarkus pour obtenir ce bogue. @DavideD n'avait qu'à faire une configuration spéciale pour reproduire en simple HR (sans Quarkus).

Sûr.

Le fait est que ce n'est pas vraiment un bug des RH. J'ai délibérément ajouté cette chose "collecte dans le groupe d'extraction par défaut" au cœur spécifiquement parce que nous en avons besoin tout le temps dans les RH.

Le bug est donc dans l'extension Quarkus qui le désactive.

(Ou, sans doute, dans le noyau pour faire la mauvaise chose par défaut.)

J'ai envoyé un correctif pour Quarkus : https://github.com/quarkusio/quarkus/pull/15818
getReference ne semble pas fonctionner sur quarkus pour le moment. Je vais créer un problème séparé pour cela.

Ça a l'air super, merci ! ??

Je viens de le tester et je peux confirmer que la récupération de collection paresseuse fonctionne désormais dans Quarkus. Merci encore :sourire:

Merci @markusdlugi

Merci de l'avoir signalé @markusdlugi

Cette page vous a été utile?
0 / 5 - 0 notes