Подтвердить что ты не робот

Как перенести LARGE BLOB (> 100MB) в Oracle с использованием Hibernate

Я пытаюсь найти способ вставки больших изображений ( > 100 МБ, в основном формата TIFF) в моей базе данных Oracle, используя столбцы BLOB.

Я тщательно искал информацию в Интернете и даже в StackOverflow, не имея возможности найти ответ на эту проблему.
Прежде всего, проблема... затем короткая секция соответствующего кода (java classes/configuration), наконец, третий раздел, где я показываю тест 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)

Код (объекты домена, классы репозитория, конфигурация)

Вот стек технологий, которые я использую (от DB до уровня бизнес-логики). Я использую JDK6.

  • Oracle Database 10g Enterprise Edition Release 10.2.0.4.0 - Prod
  • ojdbc6.jar(для выпуска 11.2.0.3)
  • Hibernate 4.0.1 Final
  • Spring 3.1.GA RELEASE

У меня есть два класса домена, отображаемые по принципу "один ко многим". A 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, вызывающий ошибку

Если я запустил следующий unit test, я получил вышеупомянутую ошибку (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. Изображения записываются в базу данных. Отлаживая мой код, я смог обнаружить, что драйверы jdbc PostgreSQL записываются в базу данных с использованием буферизованного выходного потока.... в то время как драйвер OJDBC Oracle пытается выделить сразу все byte[], представляющие изображение.

Из стека ошибок:

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 установлен на 512 МБ, используя параметр -Xmx512MB
  • Для базы данных Oracle я использовал драйвер ojdbc6.jar
  • Для базы данных Postgres я использовал драйвер 9.0-801.jdbc3 (через Maven)

Первый тест с файлом около 150 МБ

В этом первом тесте Oracle и Postgres прошли тест (это БОЛЬШИЕ новости). Размер файла составляет 1/3 доступного размера кучи JVM. Здесь изображение потребления памяти JVM:

Тестирование Oracle, размер кучи размером 512 МБ, файл 150 МБ Testing Oracle, 512MB Heap Size, 150MB file

Тестирование PostgreSQL, размер кучи размером 512 МБ, файл 150 МБ Testing PostgreSQL, 512MB Heap Size, 150MB file

Второй тест с файлом около 485 МБ

В этом втором тесте только Postgres прошли тест, а Oracle отказался. Размер файла очень близок к размеру доступного пространства кучи JVM. Здесь изображение потребления памяти JVM:

Тестирование Oracle, размер кучи размером 512 МБ, файл 485 МБ Testing Oracle, 512MB Heap Size, 485MB file

Тестирование PostgreSQL, размер кучи размером 512 МБ, файл 485 МБ Testing PostgreSQL, 512MB Heap Size, 485MB file

Анализ тестов:

Кажется, что драйвер PostgreSQL обрабатывает память без превышения определенного порога, в то время как драйвер Oracle ведет себя по-разному.

Я не могу честно объяснить, почему драйвер Oracle jdbc приводит меня к ошибке (тот же java.lang.OutOfMemoryError: Java heap space) при использовании размера файла около доступного пространства кучи.

Есть ли кто-нибудь, кто может дать мне больше информации? Большое спасибо за помощь:)

4b9b3361

Ответ 1

У меня были те же проблемы, что и при попытке сопоставить, используя тип "blob". Вот ссылка на сообщение, которое я сделал на сайте hibernate: https://forum.hibernate.org/viewtopic.php?p=2452481#p2452481

Спящий режим 3.6.9
Oracle Driver 11.2.0.2.0
Oracle Database 11.2.0.2.0

Чтобы исправить проблему, я использовал код, который имел пользовательский тип UserType для Blob, у меня был тип возвращаемого значения 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);
   }
}

Ответ 2

Лично я храню файлы до 200 МБ в столбцах Oracle BLOB, используя Hibernate, поэтому я могу заверить, что он работает. Так что...

Вы должны попробовать более новую версию драйвера JDBC Oracle. Похоже, что это поведение использования байтовых массивов вместо потоков немного изменилось со временем. И драйверы обратно совместимы. Я не уверен, если это исправит вашу проблему, но это работает для меня. Кроме того, вы должны переключиться на org.hibernate.dialect.Oracle10gDialect - который удаляет использование пакета oracle.jdbc.driver в пользу oracle.jdbc - и это также может помочь.

Ответ 3

Это не лучшее решение, но вы можете позволить Java использовать больше памяти с параметром -Xmx parametr

Изменить: Вы должны попытаться более подробно проанализировать проблему, попробуйте использовать JConsole. Это поможет вам увидеть загрузку памяти.

Даже с Postgres вы можете получить ограничение размера кучи, но не пересечь его, потому что загруженный драйвер занимает немного меньше памяти.

При настройках по умолчанию ваш размер размера кисти составляет примерно половину вашей физической памяти. Попробуйте увеличить размер блоба, который вы можете сохранить в postgres.

Ответ 4

Я только что открыл этот вопрос, когда у меня была такая же проблема с Oracle и Hibernate. Проблема заключается в обработке спящего режима Hibernate. Кажется, он копирует blob в память в зависимости от используемого Dialect. Я предполагаю, что они делают это, потому что это требуется для некоторых баз данных/драйверов. Однако для Oracle это поведение не требуется.

Исправление довольно простое, просто создайте собственный OracleDialect, содержащий этот код:

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

Затем вам нужно настроить сеанс factory для использования этого диалекта. Я протестировал его с помощью драйвера ojdbc6-11.2.0.1.0 для Oracle 11g и подтвердил, что это устраняет проблему с потреблением памяти.

Если некоторые из вас пробуют это с другой базой данных Oracle и/или с другим драйвером Oracle, я бы с удовольствием услышал, работает ли он для вас. Если он работает с несколькими конфигурациями, я отправлю запрос на перенос в команду Hibernate.

Ответ 5

Вы пытались определить LobHandler и его версию для oracle OracleLobHandler на вашем сеансе factory?

Вот пример:

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

UPDATE

Я только что понял, что речь идет о спящем 4.