Как сохранить БОЛЬШИЕ BLOB-объекты (> 100 МБ) в Oracle, используя Hibernate - PullRequest
33 голосов
/ 13 февраля 2012

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

Я провел тщательный поиск в Интернете и даже в StackOverflow, не найдя ответа на эту проблему.
Прежде всего, проблема ... затем короткий раздел о соответствующем коде (классы / конфигурация 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 Database 10g Enterprise Edition, выпуск 10.2.0.4.0 - Версия
  • 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, вызывающий ошибку

Если я запускаю следующий модульный тест, у меня появляется вышеупомянутая ошибка (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 записывают в базу данных с использованием буферизованного потока вывода ... в то время как драйвер Oracle OJDBC пытается выделить сразу все 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. Тестовая настройка:

  1. Тест проводился с использованием теста JUnit, описанного в предыдущем разделе.
  2. Размер кучи JVM был установлен на 512 МБ с использованием параметра -Xmx512MB
  3. Для базы данных Oracle я использовал ojdbc6.jar драйвер
  4. Для базы данных 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) при использовании файла размером около доступного пространства кучи.

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

Ответы [ 5 ]

6 голосов
/ 16 февраля 2012

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

Hibernate 3.6.9
Драйвер Oracle 11.2.0.2.0
База данных Oracle 11.2.0.2.0

Чтобы исправить проблему, я использовал код, который имел пользовательский тип пользователя для BLOB-объекта, у меня был возвращаемый тип java.sql.Blob.

Вот ключевые реализации этого типа:

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);
   }
}
4 голосов
/ 13 февраля 2012

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

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

2 голосов
/ 12 октября 2012

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

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

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

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

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

1 голос
/ 13 февраля 2012

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

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

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

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

0 голосов
/ 13 февраля 2012

Вы пытались определить LobHandler и его версию для oracle OracleLobHandler на своей фабрике сеансов?

Вот пример:

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...