Hibernate-reactive: coleta preguiçosa em Quarkus

Criado em 12 mar. 2021  ·  30Comentários  ·  Fonte: hibernate/hibernate-reactive

Ao tentar buscar uma associação preguiçosamente com Mutiny no Quarkus, não funciona se a busca for realizada em uma sessão diferente daquela usada para persistir os dados. Se a mesma sessão for usada (buscar logo após persistir na mesma sessão), funciona.

Comportamento esperado
Funciona também em diferentes sessões.

Comportamento real
Uma exceção é lançada ao tentar buscar a associação:

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)

Reprodutor

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

Passos para reproduzir o comportamento:

  1. Inicie o banco de dados usando infrastructure/docker-compose.yml .
  2. Inicie o aplicativo Quarkus usando mvn quarkus:dev
  3. Enviar pedido: POST http://localhost:8080/test/working .
    3.1. Isso funciona e retorna uma lista de livros.
  4. Enviar solicitação: GET http://localhost:8080/test/failing/1 (ou outro authorId criado por meio de POST http://localhost:8080/test/failing ).
    4.1 Isso falha com a exceção acima.
  • Versão Quarkus
  • Versão reativa do Hibernate: 1.0.0.Beta4 (também testado com 1.0.0.CR1 sobrescrevendo a versão no Maven, mesmo comportamento)
  • Versão do núcleo do Hibernate: 5.4.28.Final (também testado com 5.4.29.Final sobrescrevendo a versão no Maven, mesmo comportamento)
bug

Comentários muito úteis

Acabei de testar e posso confirmar que a busca preguiçosa de coleção agora funciona no Quarkus. Obrigado mais uma vez: sorria:

Todos 30 comentários

/ cc @DavideD @gavinking

Sim, acho que decidimos que essa era a coisa certa a fazer: no JPA, não há noção de reassociar uma instância de entidade a uma sessão. Isso é um pouco diferente do padrão de uso realmente antigo do Hibernate, onde você poderia usar saveOrUpdate() para reassociar diretamente um objeto separado. Em JPA, o modelo para lidar com objetos desanexados é a operação merge() .

(E há boas razões para não ter isso. A experiência é que, na prática, os usuários se metem em uma confusão com a reassociação.)

Visto que carregar uma coleção desanexada realmente implica em reconectar seu pai, simplesmente não oferecemos suporte para isso.

No entanto, IIRC, eu deixá-lo fazer isso em um StatelessSession , já que é muito mais compatível com o modelo de programação. Tente usar uma sessão sem estado e me diga se funciona para você.

Olá @gavinking , obrigado pela resposta rápida.

Não tenho certeza se estou entendendo o que você está dizendo. Então, o que implementei foi exatamente o que está descrito na documentação :

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

Isso funcionará se o autor e seus livros persistirem na mesma sessão (ou seja, na mesma solicitação HTTP), mas não funcionará se eu usar outra sessão (ou seja, outra solicitação).

Então você está insinuando que este é o comportamento pretendido, pelo menos para uma sessão "normal" (com estado?)? Este me parece um caso de uso realmente padrão, porque quase sempre tentarei obter alguns dados do banco de dados algum tempo depois de persisti-los, ou seja, em uma sessão diferente. Isso significa que, usando uma sessão normal, as associações lazy não são suportadas. Nesse caso, eu diria que deve ser explicitamente mencionado na documentação que um StatelessSession precisa ser usado se associações lazy devem ser obtidas.

Estou dizendo que na nova sessão você deve usar getReference() para obter uma referência ligada à sessão para a entidade que possui a coleção. Então você pode buscar a coleção como quiser.

Este é o modelo JPA padrão, é ainda um pouco mais fácil de usar em RH porque sobrecarreguei getReference() .

@gavinking , você quer dizer algo assim?

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

Claro, mas tornei ainda mais fácil. Você pode passar diretamente o autor destacado para getReference() .

Ok, tentei essas sugestões, mas infelizmente também não funcionaram. Talvez você possa fazer a gentileza de apontar se estou fazendo algo incorreto aqui:

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

A abordagem usando getReference() falha com a seguinte exceção:

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)
    ...

A outra solução sugerida usando StatelessSession falha com:

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)
    ...

Vou dar uma olhada, parece relacionado a melhorias de bytecode

@DavideD , alguma atualização?

Tentei reproduzir o problema sem o Quarkus usando apenas session-example neste repositório, mas não consegui. Com apenas o Hibernate Reactive e o simples Mutiny, ele funciona conforme o esperado. Também funciona usando find() , sem necessidade de getReference() ou StatelessSession .

Portanto, esse problema só parece acontecer em Quarkus.

Ainda estou investigando.

O problema ocorre por causa dos aprimoramentos de bytecode que são habilitados por padrão no Quarkus.
Eu ainda não descobri qual conjunto de propriedades recriam o problema ao usar apenas o Hibernate Reactive. Mas vou continuar a trabalhar nisso hoje

Agora posso recriar o erro no reativo. Requer duas coisas:

  1. defina hibernate.bytecode.allow_enhancement_as_proxy como verdadeiro
  2. defina SessionFactoryOptions#enableCollectionInDefaultFetchGroup como falso. Deve ser definido como verdadeiro: https://github.com/hibernate/hibernate-reactive/blob/a3f4c61eef71baab8ca321da5af0bfb9052bde4b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/suildertiveService.jpg # L27

@gavinking Estou correto em pensar que devemos ter certeza de que enableCollectionInDefaultFetchGroup no Quarkus está definido como falso? Admito que, no momento, não entendo totalmente a questão (tentei acompanhar a conversa sobre a questão 374)

Hum. Acho que só faz sentido ter a coleção no grupo de busca padrão. Acho que nunca faz sentido excluí-lo.

n Quarkus enableCollectionInDefaultFetchGroup está definido como falso

Desculpe, sim, pretendia escrever is set to true . Tempo para uma pausa :-)

Ah OK, bom :-) Faça uma pausa :-)

@DavideD Essas são ótimas descobertas, obrigado :)

Só queria salientar que, de acordo com a discussão em # 374 e o hibernate / hibernate-orm # 3558, parece que enableCollectionInDefaultFetchGroup foi deliberadamente deixado para ser false no Quarkus? Pelo menos é o que estou concluindo neste comentário:

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

De qualquer forma, parece que o Quarkus atualmente não define essa propriedade como true , porque ela não está configurada em FastBootReactiveEntityManagerFactoryBuilder :

https://github.com/quarkusio/quarkus/blob/55cf15173ff7b2985a8422de050d1c9c708be57c/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/ManotootBuilderBastaForto.JavaLava26BuilderBuilderBauloFaula26ava26avaBavaLava26Aava26Fava-UserLava268AavaBuilderBoote26avaBava26avaFaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa26aa .

E se eu verifiquei corretamente, o padrão no Hibernate ORM ainda parece ser false , o que explicaria porque vemos esse comportamento apenas no Quarkus.

@Sanne precisa dar uma olhada nisso.

Se apenas tivermos que definir como verdadeiro, parece muito fácil de resolver. Vou criar uma correção e depois verificar com @Sanne

Bem, o que estou me perguntando é se ele está definido como falso porque é disso que o ORM precisa, e se precisamos ter cuidado para não pisar no Hibernate normal.

ótimo achado, obrigado. Sim, é um pouco complicado porque as duas extensões ("regular" e "reativa") são ATM altamente acopladas.

Precisa separá-los adequadamente, que foi desenvolvido com muita pressa.

(OTOH, não sei por que o ORM precisaria dele; como argumentei antes, acho que deveria estar ativado por padrão.)

hum ok. Vamos fazer isso para ambos os ORMs (no Quarkus).

@gavinking , acho que o título está errado, não é? Eu acho que você pretendia colocar enableCollectionInDefaultFetchGroup lá em vez disso. Melhorias de bytecode são necessárias para Quarkus de qualquer maneira por causa do nativo, eu presumo :)

Sim, claro, acho que você está certo.

você está dizendo que vê este bug com as configurações

Sim, nenhuma configuração especial é necessária no Quarkus para obter este bug. @DavideD apenas teve que fazer algumas configurações especiais para reproduzir em HR simples (sem Quarkus).

Certo.

A questão é que na verdade não é um bug no RH. Eu deliberadamente adicionei aquela coisa de "coleção no grupo de busca padrão" ao núcleo especificamente porque precisamos dela o tempo todo no RH.

Portanto, o bug está na extensão Quarkus que o desliga.

(Ou, possivelmente, em essência por fazer a coisa errada por padrão.)

Enviei uma correção para o Quarkus: https://github.com/quarkusio/quarkus/pull/15818
getReference não parece funcionar com quarkus no momento. Vou criar um problema separado para ele.

Parece ótimo, obrigado! 👍

Acabei de testar e posso confirmar que a busca preguiçosa de coleção agora funciona no Quarkus. Obrigado mais uma vez: sorria:

Obrigado @markusdlugi

Obrigado por relatar isso @markusdlugi

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

gavinking picture gavinking  ·  16Comentários

Sanne picture Sanne  ·  12Comentários

DavideD picture DavideD  ·  37Comentários

yaakov-berkovitch picture yaakov-berkovitch  ·  16Comentários

aguibert picture aguibert  ·  28Comentários