Использование столбца Oracle XMLType в спящем режиме

Мне нужно сопоставить столбец Oracle XMLType с классом объектов гибернации. Существует рабочее (и я думаю, хорошо известное) решение, которое включает в себя реализацию UserType; однако я не могу использовать его, потому что требует импорта парсеров Oracle xml, что, в свою очередь, вызывает много проблем.
Я в порядке доступа к значению столбца xml в виде строки и оставляю преобразование в коде, который управляет сущностью, но я не могу найти способ прочитать значение и записать его в базу данных. Что я уже пробовал:

  1. Объявление свойства в классе сущности как String, Результат - значение читается как null, Если собственность просто SerializableЯ получаю исключение "не могу десериализовать".
  2. С помощью @Formula аннотация (CAST xmlCol as varchar2(1000)). Результат - значение не сохраняется
  3. С помощью @Loader и положить CAST в SELECT, Это была самая многообещающая попытка - значение было прочитано и успешно сохранено, но когда дело доходит до загрузки коллекции сущностей, содержащих столбец xml, я получаю null (Hibernate не использует SQL в @Loader если основная таблица LEFT JOINред).

Другой подход, который, я считаю, должен работать, - это иметь столбец XML как String (для записи) плюс пустое поле для чтения с @Formula; однако, для меня это выглядит грязным хаком, и я бы предпочел этого не делать, если у меня нет выбора.

Наконец, самое последнее, что я могу сделать, - это изменить схему БД (также более 1 опции, например, view + triggers, изменение типа данных столбца), но для меня это тоже не очень хороший вариант.

Интересно, я что-то пропустил или, может быть, есть способ заставить (3) работать?

4 ответа

Мое направление и требования

  • Сущность должна хранить XML в виде строки (java.lang.String)
  • База данных должна сохранять XML в столбце XDB.XMLType
    • Позволяет индексировать и более эффективные запросы типа xpath/ExtractValue/xquery
  • Консолидировать дюжину или около того частичных решений, которые я нашел за последнюю неделю
  • Рабочая обстановка
    • Oracle 11g r2 x64
    • Hibernate 4.1.x
    • Java 1.7.x x64
    • Windows 7 Pro x64

Пошаговое решение

Шаг 1: найдите xmlparserv2.jar (~1350 КБ)

Этот jar-файл необходим для компиляции шага 2 и включен в установку Oracle: %ORACLE_11G_HOME%/LIB/xmlparserv2.jar

Шаг 1.5: Найдите xdb6.jar (~257 КБ)

Это очень важно, если вы используете Oracle 11gR2 11.2.0.2 или выше или храните в виде BINARY XML.

Зачем?

Шаг 2. Создайте спящий пользовательский тип для столбца XMLType.

С Oracle 11g и Hibernate 4.x это проще, чем кажется.

public class HibernateXMLType implements UserType, Serializable {
static Logger logger = Logger.getLogger(HibernateXMLType.class);


private static final long serialVersionUID = 2308230823023l;
private static final Class returnedClass = String.class;
private static final int[] SQL_TYPES = new int[] { oracle.xdb.XMLType._SQL_TYPECODE };

@Override
public int[] sqlTypes() {
    return SQL_TYPES;
}

@Override
public Class returnedClass() {
    return returnedClass;
}

@Override
public boolean equals(Object x, Object y) throws HibernateException {
    if (x == null && y == null) return true;
    else if (x == null && y != null ) return false;
    else return x.equals(y);
}


@Override
public int hashCode(Object x) throws HibernateException {
    return x.hashCode();
}

@Override
public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {

    XMLType xmlType = null;
    Document doc = null;
    String returnValue = null;
    try {
        //logger.debug("rs type: " + rs.getClass().getName() + ", value: " + rs.getObject(names[0]));
        xmlType = (XMLType) rs.getObject(names[0]);

        if (xmlType != null) {
            returnValue = xmlType.getStringVal();
        }
    } finally {
        if (null != xmlType) {
            xmlType.close();
        }
    }
    return returnValue;
}

@Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {

    if (logger.isTraceEnabled()) {
        logger.trace("  nullSafeSet: " + value + ", ps: " + st + ", index: " + index);
    }
    try {
        XMLType xmlType = null;
        if (value != null) {
            xmlType = XMLType.createXML(getOracleConnection(st.getConnection()), (String)value);
        }
        st.setObject(index, xmlType);
    } catch (Exception e) {
        throw new SQLException("Could not convert String to XML for storage: " + (String)value);
    }
}


@Override
public Object deepCopy(Object value) throws HibernateException {
    if (value == null) {
        return null;
    } else {
        return value;
    }
}

@Override
public boolean isMutable() {
    return false;
}

@Override
public Serializable disassemble(Object value) throws HibernateException {
    try {
        return (Serializable)value;
    } catch (Exception e) {
        throw new HibernateException("Could not disassemble Document to Serializable", e);
    }
}

@Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {

    try {
        return (String)cached;
    } catch (Exception e) {
        throw new HibernateException("Could not assemble String to Document", e);
    }
}

@Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
    return original;
}



private OracleConnection getOracleConnection(Connection conn) throws SQLException {
    CLOB tempClob = null;
    CallableStatement stmt = null;
    try {
        stmt = conn.prepareCall("{ call DBMS_LOB.CREATETEMPORARY(?, TRUE)}");
        stmt.registerOutParameter(1, java.sql.Types.CLOB);
        stmt.execute();
        tempClob = (CLOB)stmt.getObject(1);
        return tempClob.getConnection();
    } finally {
        if ( stmt != null ) {
            try {
                stmt.close();
            } catch (Throwable e) {}
        }
    }
}   

Шаг 3: Аннотируйте поле в вашей сущности.

Я использую аннотации для Spring / Hibernate, а не для отображения файлов, но я думаю, что синтаксис будет похожим.

@Type(type="your.custom.usertype.HibernateXMLType")
@Column(name="attribute_xml", columnDefinition="XDB.XMLTYPE")
private String attributeXml;

Шаг 4: устранение ошибок appserver/junit в результате Oracle JAR

После включения%ORACLE_11G_HOME%/LIB/xmlparserv2.jar (1350kb) в ваш путь к классам для решения ошибок компиляции вы теперь получаете ошибки времени выполнения с вашего сервера приложений...

http://www.springframework.org/schema/beans/spring-beans-3.1.xsd<Line 43, Column 57>: XML-24509: (Error) Duplicated definition for: 'identifiedType'
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd<Line 61, Column 28>: XML-24509: (Error) Duplicated definition for: 'beans'
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd<Line 168, Column 34>: XML-24509: (Error) Duplicated definition for: 'description'
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd<Line 180, Column 29>: XML-24509: (Error) Duplicated definition for: 'import'
... more ...

ПОЧЕМУ ОШИБКИ?

Xmlparserv2.jar использует API служб JAR (механизм поставщика услуг) для изменения классов javax.xml по умолчанию, используемых для SAXParserFactory, DocumentBuilderFactory и TransformerFactory.

КАК ЭТО СЛУЧИЛОСЬ?

Javax.xml.parsers.FactoryFinder ищет пользовательские реализации, проверяя в этом порядке переменные среды, %JAVA_HOME%/lib/jaxp.properties, затем файлы конфигурации в META-INF/services на пути к классам, перед использованием реализации по умолчанию включены в JDK (com.sun.org.*).

Внутри xmlparserv2.jar существует каталог META-INF/services, который выбирает класс javax.xml.parsers.FactoryFinder. Файлы следующие:

META-INF/services/javax.xml.parsers.DocumentBuilderFactory (which defines oracle.xml.jaxp.JXDocumentBuilderFactory as the default)
META-INF/services/javax.xml.parsers.SAXParserFactory (which defines oracle.xml.jaxp.JXSAXParserFactory as the default)
META-INF/services/javax.xml.transform.TransformerFactory (which defines oracle.xml.jaxp.JXSAXTransformerFactory as the default)

РЕШЕНИЕ?

Переключите все 3 обратно, иначе вы увидите странные ошибки.

  • javax.xml.parsers. * исправить видимые ошибки
  • javax.xml.transform.* исправляет более тонкие ошибки синтаксического анализа XML
    • в моем случае, при чтении / записи конфигурации Apache Commons

БЫСТРОЕ РЕШЕНИЕ для решения ошибок запуска сервера приложений: Аргументы JVM

Чтобы переопределить изменения, сделанные xmlparserv2.jar, добавьте следующие свойства JVM в аргументы запуска сервера приложений. Логика java.xml.parsers.FactoryFinder будет сначала проверять переменные среды.

-Djavax.xml.parsers.SAXParserFactory=com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl -Djavax.xml.parsers.DocumentBuilderFactory=com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl -Djavax.xml.transform.TransformerFactory=com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl

Однако, если вы запустите тестовые примеры, используя @RunWith(SpringJUnit4ClassRunner.class) или аналогичный, вы все равно будете сталкиваться с ошибкой.

ЛУЧШЕЕ РЕШЕНИЕ ошибок запуска сервера приложений И ошибок тестового примера? 2 варианта

Вариант 1. Используйте аргументы JVM для сервера приложений и операторы @BeforeClass для тестовых случаев.

System.setProperty("javax.xml.parsers.DocumentBuilderFactory","com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");
System.setProperty("javax.xml.parsers.SAXParserFactory","com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl");
System.setProperty("javax.xml.transform.TransformerFactory","com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl");

Если у вас много тестов, это становится болезненным. Даже если вы положите это в супер.

Вариант 2. Создайте свои собственные файлы определений поставщика услуг в пути к классам компиляции / среды выполнения для вашего проекта, который переопределит файлы, включенные в xmlparserv2.jar

В проекте maven spring переопределите параметры xmlparserv2.jar, создав следующие файлы в каталоге%PROJECT_HOME%/src/main/resources:

%PROJECT_HOME%/src/main/resources/META-INF/services/javax.xml.parsers.DocumentBuilderFactory (which defines com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl as the default)
%PROJECT_HOME%/src/main/resources/META-INF/services/javax.xml.parsers.SAXParserFactory (which defines com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl as the default)
%PROJECT_HOME%/src/main/resources/META-INF/services/javax.xml.transform.TransformerFactory (which defines com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl as the default)

На эти файлы ссылаются как сервер приложений (аргументы JVM не требуются), так и решаются любые проблемы модульного тестирования, не требующие каких-либо изменений кода.

Готово.

Существует еще более простое решение для этого. Просто используйте аннотацию ColumnTransformer.

@ColumnTransformer(read = "to_clob(data)", write = "?")
@Column( name = "data", nullable = false, columnDefinition = "XMLType" )
private String data;`

Чтобы еще больше упростить ответ Celso, можно избежать создания пользовательской функции с помощью встроенной функции Oracle.

XMLType.CreateXML (?)

который может обрабатывать NULL.

Поэтому следующие аннотации в сочетании с пользовательским классом диалекта Celso работают хорошо.

    @Lob
    @ColumnTransformer(read = "NVL2(EVENT_DETAILS, (EVENT_DETAILS).getClobVal(), NULL)", write = "XMLType.createxml(?)")
    @Column(name = "EVENT_DETAILS")
    private String details;

Возможно, вам также придется зарегистрировать clob как xmltype на вашем собственном диалекте. Таким образом, у вас будет следующее:

public class OracleDialectExtension extends org.hibernate.dialect.Oracle10gDialect {
    public OracleDialectExtension() {
        super();
        registerColumnType(Types.CLOB, "xmltype");
    }

    @Override
    public boolean useInputStreamToInsertBlob() {
        return false;
    }
}

Убедитесь, что вы установили свой собственный диалект в списке свойств фабрики сеанса вашей конфигурации Hibernate:

<property name="hibernate.dialect"><!-- class path to custom dialect class --></property>

Попробовав много разных подходов без удачи, я придумал это:

На мой класс сущности:

@ColumnTransformer(read = "NVL2(EVENT_DETAILS, (EVENT_DETAILS).getClobVal(), NULL)", write = "NULLSAFE_XMLTYPE(?)")
@Lob
@Column(name="EVENT_DETAILS")
private String details;

Обратите внимание на круглые скобки вокруг "EVENT_DETAILS". Если вы их не поместите, Hibernate не перезапишет имя столбца, добавив имя таблицы слева.

Вам нужно будет создать функцию NULLSAFE_XMLTYPE, которая позволит вам вставлять нулевые значения (поскольку существует ограничение только в один вопросительный знак для преобразования записи в @ColumnTransformer, а XMLType(NULL) создает исключение). Я создал функцию следующим образом:

create or replace function NULLSAFE_XMLTYPE (TEXT CLOB) return XMLTYPE IS
    XML XMLTYPE := NULL;
begin
    IF TEXT IS NOT NULL THEN
      SELECT XMLType(TEXT) INTO XML FROM DUAL;
    END IF;

    RETURN XML;
end;

В моем файле persistence.xml:

<property name="hibernate.dialect" value="mypackage.CustomOracle10gDialect" />

Пользовательский диалект (если мы не переопределим метод "useInputStreamToInsertBlob", мы получим "ORA-01461: можно связать значение LONG только для вставки в столбец LONG"):

package mypackage;

import org.hibernate.dialect.Oracle10gDialect;

public class CustomOracle10gDialect extends Oracle10gDialect {

    @Override
    public boolean useInputStreamToInsertBlob() { 
        //This forces the use of CLOB binding when inserting
        return false;
    }
}

Это работает для меня, используя Hibernate 4.3.6 и Oracle 11.2.0.1.0 (с ojdbc6-11.1.0.7.0.jar).

Я должен признать, что я не пробовал решение Matt M, потому что оно включает в себя много хаков и использование библиотек, которые не входят в стандартные репозитории Maven.

Решение Камуффеля было моей отправной точкой, но я получил ошибку ORA-01461, когда попытался вставить большой XML, поэтому мне пришлось создать свой собственный диалект. Кроме того, я обнаружил проблемы с подходом TO_CLOB(XML_COLUMN) (я получал бы ошибки "ORA-19011: слишком мал символьный буфер строки"). Я предполагаю, что таким образом значение XMLTYPE сначала преобразуется в VARCHAR2, а затем в CLOB, что вызывает проблемы при попытке чтения больших XML-файлов. Вот почему после некоторых исследований я решил использовать XML_COLUMN.getClobVal().

Я не нашел это точное решение в Интернете. Вот почему я решил создать учетную запись Stackru, чтобы публиковать ее на случай, если она может быть полезна кому-то еще.

Я использую JAXB для построения XML-строки, но я думаю, что в данном случае это не актуально.

У меня возникла проблема при переходе с Hibernate 3.6.* На Hibernate 5.4, я решил, добавив зависимость dbUnit maven до Oracle xmlparserv2. dbUnit имеет xerces:xercesImpl как временную зависимость. Таким образом, мне не нужно возиться с конфигурацией сервера приложений, и модульные тесты работают нормально.

Другие вопросы по тегам