如何使用 Hibernate 在 Oracle 中保留 LARGE BLOB (>100MB)

Posted

技术标签:

【中文标题】如何使用 Hibernate 在 Oracle 中保留 LARGE BLOB (>100MB)【英文标题】:How to persist LARGE BLOBs (>100MB) in Oracle using Hibernate 【发布时间】:2012-03-04 10:19:15 【问题描述】:

我正在努力寻找一种使用 BLOB 列在我的 Oracle 数据库中插入 LARGE 图像(>100MB,主要是 TIFF 格式)的方法。

我已经在整个网络甚至 *** 中进行了彻底的搜索,但无法找到这个问题的答案。 首先,问题...然后是相关代码的一小部分(java 类/配置),最后是 第三部分,我在其中展示了我为测试图像持久性而编写的 junit 测试(我在 junit 测试执行期间收到错误)

编辑:我在问题末尾添加了一个部分,我在其中描述了一些使用 JConsole 进行的测试和分析

问题

我在使用休眠时收到java.lang.OutOfMemoryError: Java heap space 错误并尝试保留非常大的图像/文档:

java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2786)
at java.io.ByteArrayOutputStream.toByteArray(ByteArrayOutputStream.java:133)
at org.hibernate.type.descriptor.java.DataHelper.extractBytes(DataHelper.java:190)
at org.hibernate.type.descriptor.java.BlobTypeDescriptor.unwrap(BlobTypeDescriptor.java:123)
at org.hibernate.type.descriptor.java.BlobTypeDescriptor.unwrap(BlobTypeDescriptor.java:47)
at org.hibernate.type.descriptor.sql.BlobTypeDescriptor$4$1.doBind(BlobTypeDescriptor.java:101)
at org.hibernate.type.descriptor.sql.BasicBinder.bind(BasicBinder.java:91)
at org.hibernate.type.AbstractStandardBasicType.nullSafeSet(AbstractStandardBasicType.java:283)
at org.hibernate.type.AbstractStandardBasicType.nullSafeSet(AbstractStandardBasicType.java:278)
at org.hibernate.type.AbstractSingleColumnStandardBasicType.nullSafeSet(AbstractSingleColumnStandardBasicType.java:89)
at org.hibernate.persister.entity.AbstractEntityPersister.dehydrate(AbstractEntityPersister.java:2184)
at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2430)
at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2874)
at org.hibernate.action.EntityInsertAction.execute(EntityInsertAction.java:79)
at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:273)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:265)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:184)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:51)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1216)
at it.paoloyx.blobcrud.manager.DocumentManagerTest.testInsertDocumentVersion(DocumentManagerTest.java:929)

代码(域对象、存储库类、配置)

这是我正在使用的技术堆栈(从数据库到业务逻辑层)。我用的是JDK6。

Oracle 数据库 10g 企业版版本 10.2.0.4.0 - 产品 ojdbc6.jar(适用于 11.2.0.3 版本) 休眠 4.0.1 最终版 春季 3.1.GA 发布

我有两个域类,以一对多的方式映射。一个DocumentVersion 有多个DocumentData,每个DocumentVersion 可以代表不同的二进制内容。

DocumentVersion 类的相关摘录:

@Entity
@Table(name = "DOCUMENT_VERSION")
public class DocumentVersion implements Serializable 

private static final long serialVersionUID = 1L;
private Long id;
private Set<DocumentData> otherDocumentContents = new HashSet<DocumentData>(0);


@Id
@GeneratedValue(strategy = GenerationType.TABLE)
@Column(name = "DOV_ID", nullable = false)
public Long getId() 
    return id;


@OneToMany
@Cascade( CascadeType.SAVE_UPDATE )
@JoinColumn(name = "DOD_DOCUMENT_VERSION")
public Set<DocumentData> getOtherDocumentContents() 
    return otherDocumentContents;

DocumentData 类的相关摘录:

@Entity
@Table(name = "DOCUMENT_DATA")
public class DocumentData 

private Long id;

/**
 * The binary content (java.sql.Blob)
 */
private Blob binaryContent;

@Id
@GeneratedValue(strategy = GenerationType.TABLE)
@Column(name = "DOD_ID", nullable = false)
public Long getId() 
    return id;


@Lob
@Column(name = "DOD_CONTENT")
public Blob getBinaryContent() 
    return binaryContent;

这是我的 Spring 和 Hibernate 配置主要参数:

<bean id="sessionFactory"
    class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="packagesToScan" value="it.paoloyx.blobcrud.model" />
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.Oracle10gDialect</prop>
            <prop key="hibernate.hbm2ddl.auto">create</prop>
            <prop key="hibernate.jdbc.batch_size">0</prop>
            <prop key="hibernate.jdbc.use_streams_for_binary">true</prop>
        </props>
    </property>
</bean>
<bean class="org.springframework.orm.hibernate4.HibernateTransactionManager"
    id="transactionManager">
    <property name="sessionFactory" ref="sessionFactory" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />

我的数据源定义:

<bean class="org.apache.commons.dbcp.BasicDataSource"
    destroy-method="close" id="dataSource">
    <property name="driverClassName" value="$database.driverClassName" />
    <property name="url" value="$database.url" />
    <property name="username" value="$database.username" />
    <property name="password" value="$database.password" />
    <property name="testOnBorrow" value="true" />
    <property name="testOnReturn" value="true" />
    <property name="testWhileIdle" value="true" />
    <property name="timeBetweenEvictionRunsMillis" value="1800000" />
    <property name="numTestsPerEvictionRun" value="3" />
    <property name="minEvictableIdleTimeMillis" value="1800000" />
    <property name="validationQuery" value="$database.validationQuery" />
</bean>

属性取自这里:

database.driverClassName=oracle.jdbc.OracleDriver
database.url=jdbc:oracle:thin:@localhost:1521:devdb
database.username=blobcrud
database.password=blobcrud
database.validationQuery=SELECT 1 from dual

我有一个服务类,它委托给存储库类:

@Transactional
public class DocumentManagerImpl implements DocumentManager 

DocumentVersionDao documentVersionDao;

public void setDocumentVersionDao(DocumentVersionDao documentVersionDao) 
    this.documentVersionDao = documentVersionDao;

现在是存储库类的相关摘录:

public class DocumentVersionDaoHibernate implements DocumentVersionDao 

@Autowired
private SessionFactory sessionFactory;

@Override
public DocumentVersion saveOrUpdate(DocumentVersion record) 
    this.sessionFactory.getCurrentSession().saveOrUpdate(record);
    return record;

导致错误的 JUnit 测试

如果我运行以下单元测试,我会遇到上述错误 (java.lang.OutOfMemoryError: Java heap space):

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations =  "classpath*:META-INF/spring/applicationContext*.xml" )
@Transactional
public class DocumentManagerTest 

@Autowired
protected DocumentVersionDao documentVersionDao;

@Autowired
protected SessionFactory sessionFactory;

@Test
public void testInsertDocumentVersion() throws SQLException 

    // Original mock document content
    DocumentData dod = new DocumentData();
    // image.tiff is approx. 120MB
    File veryBigFile = new File("/Users/paoloyx/Desktop/image.tiff");
    try 
        Session session = this.sessionFactory.getCurrentSession();
        InputStream inStream = FileUtils.openInputStream(veryBigFile);
        Blob blob = Hibernate.getLobCreator(session).createBlob(inStream, veryBigFile.length());
        dod.setBinaryContent(blob);
     catch (IOException e) 
        e.printStackTrace();
        dod.setBinaryContent(null);
    

    // Save a document version linked to previous document contents
    DocumentVersion dov = new DocumentVersion();
    dov.getOtherDocumentContents().add(dod);
    documentVersionDao.saveOrUpdate(dov);
    this.sessionFactory.getCurrentSession().flush();

    // Clear session, then try retrieval
    this.sessionFactory.getCurrentSession().clear();
    DocumentVersion dbDov = documentVersionDao.findByPK(insertedId);
    Assert.assertNotNull("Il document version ritornato per l'id " + insertedId + " è nullo", dbDov);
    Assert.assertNotNull("Il document version recuperato non ha associato contenuti aggiuntivi", dbDov.getOtherDocumentContents());
    Assert.assertEquals("Il numero di contenuti secondari non corrisponde con quello salvato", 1, dbDov.getOtherDocumentContents().size());

相同的代码适用于 PostreSQL 9 安装。图像正在写入数据库中。 调试我的代码,我发现 PostgreSQL jdbc 驱动程序使用缓冲输出流写入数据库......而 Oracle OJDBC 驱动程序尝试一次分配所有 byte[]representing 图像。

来自错误堆栈:

java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2786)
at java.io.ByteArrayOutputStream.toByteArray(ByteArrayOutputStream.java:133)

错误是由于这种行为造成的吗? 谁能给我一些关于这个问题的见解?

谢谢大家。

使用 JConsole 进行内存测试

感谢收到的针对我的问题的建议,我尝试使用两种不同的 jdbc 驱动程序进行一些简单的测试,以显示我的代码的内存使用情况,一种用于 PostgreSQL,另一种用于 Oracle。 测试设置:

    已使用上一节中描述的 JUnit 测试进行了测试。 JVM 堆大小已设置为 512MB,使用参数 -Xmx512MB 对于 Oracle 数据库,我使用了 ojdbc6.jar 驱动程序 对于 Postgres 数据库,我使用了 9.0-801.jdbc3 驱动程序(通过 Maven)

第一次测试,文件大约 150MB

在第一次测试中,Oracle 和 Postgres通过了测试(这是个大新闻)。 该文件的大小是可用 JVM 堆大小的 1/3。 这是JVM内存消耗的图片:

测试 Oracle,512MB 堆大小,150MB 文件

测试 PostgreSQL,512MB 堆大小,150MB 文件

第二次测试,文件约485MB

在第二次测试中,只有 Postgres 通过了测试,而 Oracle 失败了。 该文件的大小非常接近可用 JVM 堆空间的大小。 这是JVM内存消耗的图片:

测试 Oracle,512MB 堆大小,485MB 文件

测试 PostgreSQL,512MB 堆大小,485MB 文件

测试分析:

似乎 PostgreSQL 驱动程序处理内存没有超过某个阈值,而 Oracle 驱动程序的行为非常不同。

当使用大小接近可用堆空间的文件时,我无法诚实地解释为什么 Oracle jdbc 驱动程序会导致我出错(同样的 java.lang.OutOfMemoryError: Java heap space)。

有没有人可以给我更多的见解? 非常感谢您的帮助:)

【问题讨论】:

+1 用于单元测试。看起来问题不像 Oracle 驱动程序那么严重......是否有任何替代的 Oracle JDBC 驱动程序可能不会那么脑残? 使用原始 JDBC 并绕过 Hibernate/JPA 进行此操作。 JDBC 允许将InputStreams 设置为参数(不是完美无缺,但它可​​以工作)。我真的希望有一个更好的答案,但是我没有找到任何与 Hibernate/JPA/Oracle 配合得很好的东西。 他的,感谢您的评论。我已经编写了一个将大图像存储在数据库中的工作类,但是使用普通 JDBC 确实是我想留下的一个选项,作为我的最终选择。我真的很想找到一种使用 Hibernate 的方法...... 非常感谢您的 +1,乔。我不确定是否有任何替代的 oracle-jdbc 驱动程序......也许不包括 DataDirect 驱动程序,但我不期待使用它们。您认为我应该购买许可证并试用吗? OutOfMemoryError 由 JVM 抛出,因为它决定它不能再使用您获得的设置(eden,tenured)增加堆大小。您可能知道,JVM 至少有 9 个参数来调整 Sun/Oracle JVM 中的堆大小(在 JRockit 中少一些)。所以这就是为什么它比你认为它应该更快地抛出 OutOfMemoryError 的原因。另一方面,上面的图表清楚地表明 PostgreSQL 驱动程序没有让 Hibernate 在上传过程中创建完整的数组。通常我的下一步是验证调用的堆栈跟踪,以查看 Hibernate 在做什么以及为什么。 【参考方案1】:

我个人使用 Hibernate 在 Oracle BLOB 列中存储高达 200MB 的文件,因此我可以确保它可以正常工作。所以...

您应该尝试更新版本的 Oracle JDBC 驱动程序。似乎随着时间的推移,这种使用字节数组而不是流的行为发生了一些变化。并且驱动程序向后兼容。我不确定这是否能解决您的问题,但它对我有用。 此外,您应该切换到 org.hibernate.dialect.Oracle10gDialect - 它不再使用 oracle.jdbc.driver 包以支持 oracle.jdbc - 它也可能会有所帮助。

【讨论】:

附加信息:查看OracleDialect 和Oracle10gDialect 的层次结构。这些实现是不同的! 好吧,Łukasz,谢谢你的回复。我尝试在我的数据源定义中使用 Oracle10gDialect 并指向“oracle.jdbc.OracleDriver”(正如您在我编辑的答案中看到的那样,我添加了我的数据源定义的一小部分)。然而,我仍然遇到这个令人沮丧的错误......可能是与 Spring 相关的问题吗?我是否应该尝试使用普通的 Hibernate 并查看是否出现相同的错误?恐怕这是与 oracle 驱动程序相关的问题...当您在数据库中存储高达 200MB 的文件时,您是否使用 Spring/Hibernate?谢谢 啊,顺便说一句,我现在正在使用最新版本的 Oracle Jdbc 驱动程序 (ojdbc6.jar) 不,我使用的是带有 Hibernate 的普通旧 DAO,而不是 Spring。 Spring 在这里可能是一个问题,但我怀疑它会是驱动程序 - 它完全支持流 blob 以及分块。另一方面,休眠可能是这里的一个问题,我会尝试看看它。 您是否可以发布或发送此上下文的休眠启动的完整日志?我想看看它是如何进行所有映射等的。如果它可以在所有级别上,那就太好了......【参考方案2】:

您是否尝试在会话工厂中为 oracle OracleLobHandler 定义 LobHandler 及其版本?

这是一个例子:

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
    <property name="dataSource" ref="oracleDocDataSource"/>
    <property name="annotatedClasses">
        <list>
        ...
        </list>
    </property>
    <property name="lobHandler">
        <bean class="org.springframework.jdbc.support.lob.OracleLobHandler">
            <property name="nativeJdbcExtractor">
                <bean class="org.springframework.jdbc.support.nativejdbc.WebSphereNativeJdbcExtractor"/>
            </property>
        </bean>
    </property>
</bean>

更新

我刚刚意识到演讲是关于hibernate 4的。

【讨论】:

是的,我正在使用 Hibernate4... :)【参考方案3】:

这不是最佳解决方案,但您可以通过 -Xmx 参数允许 Java 使用更多内存

编辑: 您应该尝试更深入地分析问题,尝试使用JConsole。它可以帮助您查看内存负载。

即使使用 Postgres,您也​​可能会接近堆大小限制,但不会超过它,因为加载的驱动程序占用的内存会少一些。

使用默认设置,您的 heam 大小限制约为物理内存的一半。试试你可以将多大的 blob 保存到 postgres 中。

【讨论】:

谢谢,Hurda,但我在 Web 应用程序中使用这个服务类(即多个用户可以同时在数据库上保存大图像)。不幸的是,扩展 JVM 内存不是我的选择:( 但是你的内存有多大?您必须将整个 BLOB 存储在您的内存中 - 才能使用它 - 出于某种原因,它看起来像是复制了这部分内存 - 它可能只需要在最佳场景中所需的两倍大小。使用两倍大小的内存还有问题吗? 好吧,在这种情况下,我没有使用 -Xmx 参数调整或扩展 JVM 内存,因为我认为这不是正确的解决方案,至少对于这种特殊情况。目前我无法预测我的系统必须支持的用户数量/并发文件上传......即使我为 JVM 设置了更多内存,我也可能需要处理更多的用户,然后我会进入“无休止”的一轮记忆调整。顺便说一句...使用 PostgreSQL 我可以存储我的图像,就像一个魅力。所以一定是Oracle和Hibernate交互中的东西 @paoloyx 你在这里做了很多假设,如果你没有尝试使用 -Xmx 进行操作,你可能会遇到问题,当两个同时请求存储图像时,你用 postrges 耗尽内存击中服务器。尝试使用 JConsole 连接到 serevr 进程,看看正在使用多少堆。 嗨,Hurda,根据您的建议,我添加了一个部分,其中显示了一些测试(和 JVM 内存使用情况)。感谢您的宝贵帮助。 :) Postgres 和 Oracle jdbc 驱动程序的行为非常不同,查看 JConsole 的输出...【参考方案4】:

在尝试使用“blob”类型进行映射时,我遇到了与您相同的问题。这是我在 hibernate 网站上发布的帖子的链接:https://forum.hibernate.org/viewtopic.php?p=2452481#p2452481

休眠 3.6.9 Oracle 驱动程序 11.2.0.2.0 Oracle 数据库 11.2.0.2.0

为了解决这个问题,我使用了具有 Blob 的自定义 UserType 的代码,我将返回类型设置为 java.sql.Blob。

这里是这个 UserType 的关键方法实现:

public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException 

   Blob blob = rs.getBlob(names[0]);
   if (blob == null)
      return null;

   return blob;


public void nullSafeSet(PreparedStatement st, Object value, int index)
     throws HibernateException, SQLException 
   if (value == null) 
      st.setNull(index, sqlTypes()[0]);
   
   else 
      InputStream in = null;
      OutputStream out = null;
      // oracle.sql.BLOB
      BLOB tempBlob = BLOB.createTemporary(st.getConnection(), true, BLOB.DURATION_SESSION);
      tempBlob.open(BLOB.MODE_READWRITE);
      out = tempBlob.getBinaryOutputStream();
      Blob valueAsBlob = (Blob) value;
      in = valueAsBlob.getBinaryStream();
      StreamUtil.toOutput(in, out);
      out.flush();
      StreamUtil.close(out);
      tempBlob.close();
      st.setBlob(index, tempBlob);
      StreamUtil.close(in);
   

【讨论】:

谢谢你,我正在阅读你在 Hibernate 论坛上的帖子...我会尝试你的建议,我会告诉你的! 这个解决方案有效吗?请给点意见好吗?【参考方案5】:

当我遇到与 Oracle 和 Hibernate 相同的问题时,我刚刚发现了这个问题。问题在于 Hibernate blob 处理。根据使用的方言,它似乎将 blob 复制到内存中。我猜他们这样做是因为某些数据库/驱动程序需要它。不过,对于 Oracle,这种行为似乎不是必需的。

修复非常简单,只需创建一个包含以下代码的自定义 OracleDialect:

public class Oracle10DialectWithoutInputStreamToInsertBlob extends Oracle10gDialect 
    public boolean useInputStreamToInsertBlob() 
        return false;
    

接下来,您需要配置会话工厂以使用此方言。我已经使用面向 Oracle 11g 的 ojdbc6-11.2.0.1.0 驱动程序对其进行了测试,并确认这解决了内存消耗问题。

如果你们中的一些人尝试使用另一个 Oracle 数据库和/或不同的 Oracle 驱动程序,我很想知道它是否适合您。如果它适用于多种配置,我将向 Hibernate 团队发送拉取请求。

【讨论】:

以上是关于如何使用 Hibernate 在 Oracle 中保留 LARGE BLOB (>100MB)的主要内容,如果未能解决你的问题,请参考以下文章

如何使用java在hibernate 3 + oracle中设置查询级别超时

如何使用现有的Oracle序列在hibernate中生成id?

如何使用 @ID 和 @GeneratedValue 从 Hibernate + JPA 中的序列中获取 Oracle 生成的值

如何让 Hibernate 在 Oracle 上将 SequenceHiLoGenerator 用于 JPA GenerationType.AUTO?

如何使用 Hibernate (EntityManager) 或 JPA 调用 Oracle 函数或过程

Hibernate 和 Oracle VARRAYS/嵌套表