Hibernate-reactive: ленивая загрузка коллекции в Quarkus

Созданный на 12 мар. 2021  ·  30Комментарии  ·  Источник: hibernate/hibernate-reactive

При попытке лениво получить ассоциацию с Mutiny в Quarkus это не сработает, если выборка выполняется в другом сеансе, чем тот, который использовался для сохранения данных. Если используется тот же сеанс (т.е. выборка сразу после сохранения в том же сеансе), он работает.

Ожидаемое поведение
Он также работает в разных сеансах.

Фактическое поведение
При попытке получить ассоциацию выдается исключение:

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)

Репродуктор

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

Шаги по воспроизведению поведения:

  1. Запустить базу данных с помощью infrastructure/docker-compose.yml .
  2. Запустите приложение Quarkus, используя mvn quarkus:dev
  3. Отправить запрос: POST http://localhost:8080/test/working .
    3.1. Это работает и возвращает список книг.
  4. Отправить запрос: GET http://localhost:8080/test/failing/1 (или другой authorId, созданный с помощью POST http://localhost:8080/test/failing ).
    4.1 Это не удается за исключением вышеупомянутого.
  • Версия Quarkus
  • Реактивная версия Hibernate: 1.0.0.Beta4 (также тестировалась с 1.0.0.CR1 путем перезаписи версии в Maven, такое же поведение)
  • Версия Hibernate Core: 5.4.28.Final (также проверено с 5.4.29.Final путем перезаписи версии в Maven, такое же поведение)

Самый полезный комментарий

Просто протестировал его и могу подтвердить, что ленивая загрузка коллекций теперь работает в Quarkus. Еще раз спасибо: smile:

Все 30 Комментарий

/ cc @DavideD @gavinking

Да, я думаю, мы решили, что это правильно: в JPA нет понятия повторного связывания экземпляра сущности с сеансом. Это немного отличается от действительно старого шаблона использования Hibernate, где вы могли использовать saveOrUpdate() для прямого повторного связывания отсоединенного объекта. В JPA моделью работы с отсоединенными объектами является операция merge() .

(И есть веские причины не иметь этого. Опыт показывает, что на практике пользователи сами попадают в затруднительное положение из-за повторной ассоциации.)

Поскольку загрузка отсоединенной коллекции на самом деле подразумевает повторное присоединение ее родительского элемента, мы просто не поддерживаем это.

Тем не менее, IIRC, я позволю вам сделать это в StatelessSession , так как это гораздо более совместимы с этой моделью программирования. Попробуйте использовать сеанс без сохранения состояния и сообщите мне, работает ли он для вас.

Привет @gavinking , спасибо за быстрый ответ.

Я не совсем уверен, понимаю ли я то, что вы говорите. Я реализовал именно то, что описано в документации :

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

Это работает, если автор и его книги сохраняются в одном и том же сеансе (то есть в одном HTTP-запросе), но не работает, если я использую другой сеанс (т.е. другой запрос).

Значит, вы подразумеваете, что это предполагаемое поведение, по крайней мере, для «нормального» (с отслеживанием состояния?) Сеанса? Мне это кажется действительно стандартным вариантом использования, потому что я почти всегда буду пытаться получить некоторые данные из базы данных через некоторое время после их сохранения, то есть в другом сеансе. Это означало бы, что при использовании обычного сеанса ленивые ассоциации не поддерживаются. В этом случае я бы сказал, что в документации следует явно упомянуть, что StatelessSession необходимо использовать, если нужно получить ленивые ассоциации.

Я говорю, что в новом сеансе вы должны использовать getReference() для получения привязанной к сеансу ссылки на объект, которому принадлежит коллекция. Затем вы можете получить коллекцию, как хотите.

Это стандартная модель JPA, ее даже немного проще использовать в HR, потому что я перегрузил getReference() .

@gavinking вы имеете в виду что-то вроде этого?

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

Конечно, но я сделал это еще проще. Вы можете напрямую передать отдельного автора в getReference() .

Хорошо, я попробовал эти предложения, но, к сожалению, они тоже не сработали. Может быть, вы будете так любезны указать, если я здесь что-то делаю неправильно:

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

Подход с использованием getReference() не работает за следующим исключением:

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

Другое предлагаемое решение с использованием StatelessSession не работает:

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

Я посмотрю, похоже, это связано с улучшением байт-кода

@DavideD , есть обновления?

Я попытался воспроизвести проблему без Quarkus, просто используя session-example в этом репо, но мне это не удалось. Только с Hibernate Reactive и простым Mutiny он работает, как ожидалось. Он также работает с использованием find() , без необходимости getReference() или StatelessSession .

Таким образом, эта проблема возникает только в Quarkus.

Я все еще изучаю это.

Проблема возникает из-за улучшений байт-кода, которые по умолчанию включены в Quarkus.
Я до сих пор не понял, какой набор свойств воссоздает проблему при использовании только Hibernate Reactive. Но я продолжу работать над этим сегодня

Теперь я могу воссоздать ошибку в реактивном режиме. Это требует двух вещей:

  1. установить для hibernate.bytecode.allow_enhancement_as_proxy значение true
  2. установите для SessionFactoryOptions#enableCollectionInDefaultFetchGroup значение false. Для него должно быть установлено значение true: https://github.com/hibernate/hibernate-reactive/blob/a3f4c61eef71baab8ca321da5af0bfb9052bde4b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/serviceSactiveSupport.pdf # L27

@gavinking Правильно ли я полагаю, что мы должны убедиться, что в Quarkus для enableCollectionInDefaultFetchGroup установлено значение false? Признаюсь, что на данный момент я не совсем понимаю всю проблему (пытался следить за разговором по проблеме №374)

Гм. Я думаю, что имеет смысл иметь коллекцию только в группе выборки по умолчанию. Я не думаю, что когда-либо имеет смысл исключать это.

n Quarkus enableCollectionInDefaultFetchGroup имеет значение false

Извините, да, я хотел написать is set to true . Время для перерыва :-)

Ну ладно, хорошо :-) Сделайте перерыв :-)

@DavideD Это отличные открытия, спасибо :)

Просто хотел отметить, что, согласно обсуждению в # 374 и соответствующем hibernate / hibernate-orm # 3558, похоже, что enableCollectionInDefaultFetchGroup намеренно оставлен как false в Quarkus? По крайней мере, это то, что я понял из этого комментария:

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

В любом случае, похоже, что Quarkus в настоящее время не устанавливает для этого свойства значение true , потому что оно не настроено в FastBootReactiveEntityManagerFactoryBuilder :

https://github.com/quarkusio/quarkus/blob/55cf15173ff7b2985a8422de050d1c9c708be57c/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtimeojava/io/quarkus/hibernate/reactive/runtimeo/bootlava.html###########################

И если я проверил это правильно, значение по умолчанию в ORM Hibernate по-прежнему кажется false , что объясняет, почему мы видим такое поведение только в Quarkus.

@Sanne нужно взглянуть на это.

Если нам нужно только установить для него значение true, это кажется довольно простым решением. Я создам исправление, а затем свяжусь с

Что ж, мне интересно, установлено ли для него значение false, потому что это то, что нужно ORM, и нужно ли нам быть осторожными, чтобы не перейти в обычный Hibernate?

отличная находка, спасибо. Да, это немного сложно, потому что два расширения («обычное» и «реактивное») представляют собой банкомат с сильной связью.

Необходимо их как следует разделить, это было разработано слишком спешно.

(OTOH, я не уверен, зачем он нужен ORM; как я уже говорил ранее, я думаю, что он должен быть включен по умолчанию.)

гул ок. Сделаем это для обоих ORM (в Quarkus).

@gavinking , я думаю, это неправильное название, не так ли? Думаю, вы хотели вместо этого поставить enableCollectionInDefaultFetchGroup . Усовершенствования байт-кода в любом случае необходимы для Quarkus из-за нативности, я полагаю :)

Ах да, конечно, я думаю, ты прав.

вы говорите, что видите эту ошибку с настройками по умолчанию в Quarkus?

Да, для устранения этой ошибки в Quarkus не требуется специальной настройки. @DavideD просто нужно было выполнить некоторую специальную настройку для воспроизведения в обычном формате HR (без Quarkus).

Конечно.

Дело в том, что на самом деле это не ошибка HR. Я специально добавил в ядро ​​эту «коллекцию в группе выборки по умолчанию», потому что она нужна нам постоянно в HR.

Итак, ошибка в расширении Quarkus, которое его отключает.

(Или, возможно, в основном за неправильные действия по умолчанию.)

Я отправил исправление для Quarkus: https://github.com/quarkusio/quarkus/pull/15818
getReference , похоже, в данный момент не работает с кваркусом. Я создам для него отдельный выпуск.

Звучит здорово, спасибо! 👍

Просто протестировал его и могу подтвердить, что ленивая загрузка коллекций теперь работает в Quarkus. Еще раз спасибо: smile:

Спасибо @markusdlugi

Спасибо, что сообщили об этом @markusdlugi

Была ли эта страница полезной?
0 / 5 - 0 рейтинги

Смежные вопросы

Sanne picture Sanne  ·  12Комментарии

aguibert picture aguibert  ·  28Комментарии

gavinking picture gavinking  ·  16Комментарии

pqab picture pqab  ·  21Комментарии

hantsy picture hantsy  ·  7Комментарии