Hibernate 6.6.x breaks Oracle Blob Streaming

I’m having an issue with streaming large files to the oracle DB using Spring Boot and Hibernate.

This issue occurs after a Spring Boot Upgrade to 3.4.1 (from 3.2.5), which includes the new minor version Hibernate 6.6.4. I’m using oracle driver ojdbc8-21.3.0.0.

I have an entity definition like this

import java.sql.Blob;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Lob;

@Entity
public class Table
{

    @Lob
    @Column(name = "data")
    private Blob data;
    
}

In order to stream a large input file to the database I’m using BlobProxy.generateProxy(in, length) to set up the blob proxy with my input stream.

Upon transaction commit I now get the following exception:

Caused by: java.lang.ClassCastException: class org.hibernate.engine.jdbc.BlobProxy cannot be cast to class oracle.jdbc.internal.OracleBlob (org.hibernate.engine.jdbc.BlobProxy and oracle.jdbc.internal.OracleBlob are in unnamed module of loader 'app')
	at oracle.jdbc.driver.OraclePreparedStatement.setBlob(OraclePreparedStatement.java:6528)
	at oracle.jdbc.driver.OraclePreparedStatementWrapper.setBlob(OraclePreparedStatementWrapper.java:153)
	at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.setBlob(HikariProxyPreparedStatement.java)
	at org.hibernate.type.descriptor.jdbc.BlobJdbcType$4$1.doBind(BlobJdbcType.java:173)
	at org.hibernate.type.descriptor.jdbc.BlobJdbcType$2$1.doBind(BlobJdbcType.java:115)
	at org.hibernate.type.descriptor.jdbc.BasicBinder.bind(BasicBinder.java:61)
	at org.hibernate.engine.jdbc.mutation.internal.JdbcValueBindingsImpl.lambda$beforeStatement$0(JdbcValueBindingsImpl.java:87)
	at java.base/java.lang.Iterable.forEach(Iterable.java:75)
	at org.hibernate.engine.jdbc.mutation.spi.BindingGroup.forEachBinding(BindingGroup.java:51)
	at org.hibernate.engine.jdbc.mutation.internal.JdbcValueBindingsImpl.beforeStatement(JdbcValueBindingsImpl.java:85)
	at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.performNonBatchedMutation(AbstractMutationExecutor.java:130)
	at org.hibernate.engine.jdbc.mutation.internal.MutationExecutorSingleNonBatched.performNonBatchedOperations(MutationExecutorSingleNonBatched.java:55)
	at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.execute(AbstractMutationExecutor.java:55)
	at org.hibernate.persister.entity.mutation.InsertCoordinatorStandard.doStaticInserts(InsertCoordinatorStandard.java:194)
	at org.hibernate.persister.entity.mutation.InsertCoordinatorStandard.coordinateInsert(InsertCoordinatorStandard.java:132)
	at org.hibernate.persister.entity.mutation.InsertCoordinatorStandard.insert(InsertCoordinatorStandard.java:104)
	at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:110)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:644)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:511)
	at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:414)
	at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:41)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
	at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1429)
	at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:491)
	at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:2354)
	at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:1978)
	at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:439)
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:169)
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:267)
	at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101)

This seems to be related to HHH-18206, and HHH-18206 Switch to JDBC LOB APIs for Oracle Dialect by loiclefevre · Pull Request #8486 · hibernate/hibernate-orm · GitHub, where the binding of the blog type changed from STREAM_BINDING to BLOB_BINDING in the org.hibernate.type.descriptor.jdbc.BlobJdbcType. While hibernate unwraps the blob to the java.sql.Blob type, the oracle driver seems to expect an instance of oracle.jdbc.internal.OracleBlob.

Is there an alternative ways to create a blob proxy now from an input stream? Or maybe an alternative way to stream large objects to the DB that works for oracle?

I’m happy to provide more details if necessary.

Please try to create a reproducer with our test case template and if you are able to reproduce the issue, create a bug ticket in our issue tracker and attach that reproducer.

@beikov

Thanks for the reference to the test case template, I forked it and was able to reproduce the issue in a simple test case. See GitHub - rascmatt/hibernate-test-case-templates: Templates and examples to report issues to Hibernate

Initially it worked fine, but the template helped me to narrow down the reproducing conditions. It seems that the exception occurs specifically when disabling the hibernate.boot.allow_jdbc_metadata_access configuration property.

I also noticed, that due to the changes in HHH-18206 Switch to JDBC LOB APIs for Oracle Dialect by loiclefevre · Pull Request #8486 · hibernate/hibernate-orm · GitHub the stream-writing of the lobs was disabled generally for Lobs in Oracle. That is, even if run with metadata_access set to true, which does not produce this exception, internally the stream is first read to a byte array, and then wrapped in an Oracle blob proxy (See org.hibernate.type.descriptor.java.BlobJavaType#getOrCreateBlob).

So, before I open a bug ticket, I’d like to discuss if this is generally discouraged now, and if there’s an alternative way of streaming data into blobs in Hibernate, which does not load everything into memory.

I find the current API (using BlobProxy.generateProxy(InputStream stream, long length)) quite misleading, because it looks like its able to handle an input stream, and not load the whole file into memory internally.

This is unfortunate. From what I can see, we would need to add a configuration option to org.hibernate.cfg.JdbcSettings to allow controlling this behavior when JDBC metadata access is disabled. Please create an improvement request for that on our issue tracker.

You have a point. I agree that the JdbcType implementations should avoid materializing the full data when the passed value is based on a stream. It seems there even was some experimentation done on that matter, but wasn’t finished. Please also create an improvement request on our issue tracker for this.