Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hibernate reactive with panache exception when lazily loading a byte array #36211

Closed
douglasss opened this issue Sep 29, 2023 · 14 comments
Closed
Assignees
Labels

Comments

@douglasss
Copy link

Describe the bug

I have the following class, with a "data" field that is a byte array.

@Entity
public class Storage extends PanacheEntity {
    @Lob
    @Basic(fetch = FetchType.LAZY)
    @Column(nullable = false)
    public byte[] data;
}

I need to load it lazily, but when I try to do it I have the following exception:

2023-09-29 08:06:55,106 ERROR [com.tha.ota.exc.ThrowableMapper] (vert.x-eventloop-thread-3) class java.util.concurrent.CompletableFuture cannot be cast to class [B (java.util.concurrent.CompletableFuture and [B are in module java.base of loader 'bootstrap'): java.lang.ClassCastException: class java.util.concurrent.CompletableFuture cannot be cast to class [B (java.util.concurrent.CompletableFuture and [B are in module java.base of loader 'bootstrap')
        at com.example.$$_hibernate_read_data(StoredFile.java)
        at com.example.StoredFile.getData(StoredFile.java)
        at com.example.StorageController.lambda$download$1(StorageController.java:83)
        at io.smallrye.context.impl.wrappers.SlowContextualConsumer.accept(SlowContextualConsumer.java:21)
        at io.smallrye.mutiny.operators.uni.UniOnItemConsume$UniOnItemComsumeProcessor.invokeEventHandler(UniOnItemConsume.java:77)
        at io.smallrye.mutiny.operators.uni.UniOnItemConsume$UniOnItemComsumeProcessor.onItem(UniOnItemConsume.java:42)
        at io.smallrye.mutiny.operators.uni.UniOnItemTransformToUni$UniOnItemTransformToUniProcessor.onItem(UniOnItemTransformToUni.java:60)
        at io.smallrye.mutiny.operators.uni.builders.DefaultUniEmitter.complete(DefaultUniEmitter.java:37)
        at io.smallrye.mutiny.groups.UniOnNull.lambda$failWith$1(UniOnNull.java:46)
        at io.smallrye.context.impl.wrappers.SlowContextualBiConsumer.accept(SlowContextualBiConsumer.java:21)
        at io.smallrye.mutiny.groups.UniOnItem.lambda$transformToUni$4(UniOnItem.java:177)
        at io.smallrye.context.impl.wrappers.SlowContextualConsumer.accept(SlowContextualConsumer.java:21)
        at io.smallrye.mutiny.operators.uni.builders.UniCreateWithEmitter.subscribe(UniCreateWithEmitter.java:22)
        at io.smallrye.mutiny.operators.AbstractUni.subscribe(AbstractUni.java:36)
        at io.smallrye.mutiny.operators.uni.UniOnItemTransformToUni$UniOnItemTransformToUniProcessor.performInnerSubscription(UniOnItemTransformToUni.java:81)
        at io.smallrye.mutiny.operators.uni.UniOnItemTransformToUni$UniOnItemTransformToUniProcessor.onItem(UniOnItemTransformToUni.java:57)
        at io.smallrye.mutiny.operators.uni.UniOnItemTransformToUni$UniOnItemTransformToUniProcessor.onItem(UniOnItemTransformToUni.java:60)
        at io.smallrye.mutiny.operators.uni.UniOperatorProcessor.onItem(UniOperatorProcessor.java:47)
        at io.smallrye.mutiny.operators.uni.builders.UniCreateFromCompletionStage$CompletionStageUniSubscription.forwardResult(UniCreateFromCompletionStage.java:63)
        at java.base/java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:863)
        at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:841)
        at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)
        at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2147)
        at org.hibernate.reactive.util.async.impl.AsyncTrampoline$TrampolineInternal.unroll(AsyncTrampoline.java:131)
        at org.hibernate.reactive.util.async.impl.AsyncTrampoline$TrampolineInternal.lambda$unroll$0(AsyncTrampoline.java:126)
        at java.base/java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:863)
        at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:841)
        at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)
        at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2147)
        at io.vertx.core.Future.lambda$toCompletionStage$3(Future.java:536)
        at io.vertx.core.impl.future.FutureImpl$3.onSuccess(FutureImpl.java:141)
        at io.vertx.core.impl.future.FutureBase.emitSuccess(FutureBase.java:60)
        at io.vertx.core.impl.future.FutureImpl.tryComplete(FutureImpl.java:211)
        at io.vertx.core.impl.future.PromiseImpl.tryComplete(PromiseImpl.java:23)
        at io.vertx.sqlclient.impl.QueryResultBuilder.tryComplete(QueryResultBuilder.java:88)
        at io.vertx.sqlclient.impl.QueryResultBuilder.tryComplete(QueryResultBuilder.java:32)
        at io.vertx.core.Promise.complete(Promise.java:66)
        at io.vertx.core.Promise.handle(Promise.java:51)
        at io.vertx.core.Promise.handle(Promise.java:29)
        at io.vertx.core.impl.future.FutureImpl$3.onSuccess(FutureImpl.java:141)
        at io.vertx.core.impl.future.FutureBase.lambda$emitSuccess$0(FutureBase.java:54)
        at io.vertx.core.impl.EventLoopContext.execute(EventLoopContext.java:86)
        at io.vertx.core.impl.DuplicatedContext.execute(DuplicatedContext.java:184)
        at io.vertx.core.impl.future.FutureBase.emitSuccess(FutureBase.java:51)
        at io.vertx.core.impl.future.FutureImpl.tryComplete(FutureImpl.java:211)
        at io.vertx.core.impl.future.PromiseImpl.tryComplete(PromiseImpl.java:23)
        at io.vertx.core.impl.future.PromiseImpl.onSuccess(PromiseImpl.java:49)
        at io.vertx.core.impl.future.PromiseImpl.handle(PromiseImpl.java:41)
        at io.vertx.core.impl.future.PromiseImpl.handle(PromiseImpl.java:23)
        at io.vertx.sqlclient.impl.command.CommandResponse.fire(CommandResponse.java:46)
        at io.vertx.sqlclient.impl.SocketConnectionBase.handleMessage(SocketConnectionBase.java:325)
        at io.vertx.sqlclient.impl.SocketConnectionBase.lambda$init$0(SocketConnectionBase.java:138)
        at io.vertx.core.impl.EventLoopContext.emit(EventLoopContext.java:55)
        at io.vertx.core.impl.ContextBase.emit(ContextBase.java:297)
        at io.vertx.core.net.impl.NetSocketImpl.handleMessage(NetSocketImpl.java:378)
        at io.vertx.core.net.impl.ConnectionBase.read(ConnectionBase.java:159)
        at io.vertx.core.net.impl.VertxHandler.channelRead(VertxHandler.java:153)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
        at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
        at io.vertx.mysqlclient.impl.codec.MySQLEncoder.handleCommandResponse(MySQLEncoder.java:63)
        at io.vertx.mysqlclient.impl.codec.QueryCommandBaseCodec.handleAllResultsetDecodingCompleted(QueryCommandBaseCodec.java:175)
        at io.vertx.mysqlclient.impl.codec.ExtendedQueryCommandBaseCodec.handleAllResultsetDecodingCompleted(ExtendedQueryCommandBaseCodec.java:51)
        at io.vertx.mysqlclient.impl.codec.QueryCommandBaseCodec.handleSingleResultsetDecodingCompleted(QueryCommandBaseCodec.java:136)
        at io.vertx.mysqlclient.impl.codec.QueryCommandBaseCodec.handleRows(QueryCommandBaseCodec.java:124)
        at io.vertx.mysqlclient.impl.codec.QueryCommandBaseCodec.decodePayload(QueryCommandBaseCodec.java:66)
        at io.vertx.mysqlclient.impl.codec.ExtendedQueryCommandCodec.decodePayload(ExtendedQueryCommandCodec.java:108)
        at io.vertx.mysqlclient.impl.codec.MySQLDecoder.decodePackets(MySQLDecoder.java:72)
        at io.vertx.mysqlclient.impl.codec.MySQLDecoder.channelRead(MySQLDecoder.java:48)
        at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
        at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346)
        at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:318)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
        at io.netty.channel.DefaultChannelPipeline$HeadContexelRead(DefaultChannelPipeline.java:1410)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
        at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
        at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
        at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788)
        at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
        at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.base/java.lang.Thread.run(Thread.java:833)

Looks like it's trying to directly cast CompletableFuture to the byte array, and I don't understand why or how to fix it.
If I change the "data" to FetchType.EAGER it works fine.

Just an example of how I'm loading the entity (It works fine with EAGER, only fails with LAZY loading the byte array) :

@GET
@Path("/{id}")
@WithSession
public Uni<byte[]> download(@PathParam("id") long id) {
    return StoredFile.<StoredFile>findById(id)
            .onItem().ifNull().failWith(NotFoundException::new)
            .invoke(s -> System.out.println(s.data))
            .map(s -> s.data);
}

Expected behavior

Load a byte array lazily

Actual behavior

Exception class java.util.concurrent.CompletableFuture cannot be cast to class [B

How to Reproduce?

No response

Output of uname -a or ver

Linux 5.15.90.1-microsoft-standard-WSL2

Output of java -version

openjdk version "17.0.7" 2023-04-18 LTS OpenJDK Runtime Environment Corretto-17.0.7.7.1 (build 17.0.7+7-LTS) OpenJDK 64-Bit Server VM Corretto-17.0.7.7.1 (build 17.0.7+7-LTS, mixed mode, sharing)

GraalVM version (if different from Java)

No response

Quarkus version or git rev

3.4.1

Build tool (ie. output of mvnw --version or gradlew --version)

Maven 3.9.2

Additional information

No response

@douglasss douglasss added the kind/bug Something isn't working label Sep 29, 2023
@quarkus-bot
Copy link

quarkus-bot bot commented Sep 29, 2023

/cc @DavideD (hibernate-reactive), @FroMage (panache), @Sanne (hibernate-reactive), @gavinking (hibernate-reactive), @loicmathieu (panache)

@DavideD
Copy link
Contributor

DavideD commented Sep 29, 2023

Hi, which database are you using? Would it be possible to have a test project I can run?

@DavideD DavideD self-assigned this Sep 29, 2023
@DavideD
Copy link
Contributor

DavideD commented Sep 29, 2023

Sorry, I think I know what the problem is.
Because this is a lazy field, you need to fetch it explicitly.
I'm going to look for an example, because I don't remember how to do it in Panache on top of my head

@douglasss
Copy link
Author

Thanks!
I'm using MySQL.
I will upload a test project in a few minutes!

@gavinking
Copy link

gavinking commented Sep 29, 2023

Sorry, I think I know what the problem is.

Because this is a lazy field, you need to fetch it explicitly.

That's exactly right, but why is the error here so ugly? We should produce an LIE here and I thought we did.

@douglasss
Copy link
Author

I've tried fetch the field with Mutiny.fetch(s.data), but had the same error, I don't know if there is another way

@DavideD
Copy link
Contributor

DavideD commented Sep 29, 2023

At the moment, fetching lazy fields is not very user friendly if you are not using the JPA static metamodel. It's a whole area that needs some improvements from a usability point of view.

@douglasss
Copy link
Author

Hi, which database are you using? Would it be possible to have a test project I can run?

I don't know if it helps, but I've created a small test project, if you run the test you can see the failure with the exception:
https://github.com/douglasss/hibernate-reactive-byte-lazy

@DavideD
Copy link
Contributor

DavideD commented Sep 29, 2023

For your particular use-case, because you only want the data field, I would use projections:

    @GET
    @Path("/{id}")
    @WithSession
    public Uni<byte[]> download(@PathParam("id") long id) {
        return StorageFile.<StorageFile>find("id", id)
                .project( FileData.class )
                .singleResult()
                .map( FileData::getData );
    }


    @RegisterForReflection
    public static class FileData {
        private final byte[] data;

        public FileData(byte[] data){
            this.data = data;
        }

        public byte[] getData() {
            return data;
        }
    }

I've tested this with your code and it seems to work.
I will add some examples on how you can use the fetch, and create an issue for Quarkus to improve this in a bit

@DavideD
Copy link
Contributor

DavideD commented Sep 29, 2023

I don't think it's possible with Panache to load lazy fields. You will have to use the session:

    private final Attribute<? super StorageFile, ?> dataAttribute;

    public StorageController(Mutiny.SessionFactory sf) {
        this.dataAttribute = sf.getMetamodel().entity( StorageFile.class ).getAttribute( "data" );
    }

    // ...

    @GET
    @Path("/{id}")
    @WithSession
    public Uni<byte[]> download(@PathParam("id") long id) {
        return Panache.getSession()
                .chain( session -> session
                        .find( StorageFile.class, id )
                        .call( file -> session.fetch( file, dataAttribute ) )
                        .map( file -> file.data )
                );
    }

If you can enable the JPA static model generation in Quarkus (I don't know how to do it), it becomes:

    @GET
    @Path("/{id}")
    @WithSession
    public Uni<byte[]> download(@PathParam("id") long id) {
        return Panache.getSession()
                .chain( session -> session
                        .find( StorageFile.class, id )
                        .chain( file -> session.fetch( file, FileStorage_.data ) )
                );
    }

I've created an issue to make Quarkus generate the model automatically, but it has not been resolved yet.

Keep in mind that using the fetch approach will cause the generation of two queries each time:

  1. One to get the entity: select s1_0.id from StorageFile s1_0 where s1_0.id=?
  2. And then one to get the data: select s1_0.data from StorageFile s1_0 where s1_0.id=?

If you use projections, like I suggested in my previous comment, it will only run one query.

I'm going to close the issue, but feel free to ask any follow up questions here.
We can also reopen it if this didn't solve your problem.

@DavideD DavideD closed this as completed Sep 29, 2023
@douglasss
Copy link
Author

Thank you for your comments!
I can solve my problem with that!

@FroMage
Copy link
Member

FroMage commented Sep 29, 2023

So, @DavideD Session.fetch(T association) only works for relations, not for blobs?

@DavideD
Copy link
Contributor

DavideD commented Sep 29, 2023

Exactly

@FroMage
Copy link
Member

FroMage commented Sep 29, 2023

OK. Yeah, I suppose you can't make array subtypes that lazy load, you must lazy load in the accessor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants