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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

  • Entity должен хранить 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(~ 1350kb)

Эта банка требуется для компиляции шага 2 и включена в инсталляцию оракула здесь: % ORACLE_11G_HOME%/LIB/xmlparserv2.jar

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

Это очень важно, если вы используете 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 используется JAR Services API (механизм поставщика услуг) для изменения классов 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)

SOLUTION?

Переключите все 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. Создайте свои собственные файлы определения поставщика услуг в пути к компиляции /runtime для вашего проекта, который переопределит те, которые включены в 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), так и решают любые проблемы unit test, не требуя каких-либо изменений кода.

Готово.

Ответ 2

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

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

Ответ 3

Попробовав множество разных подходов без везения, я придумал следующее:

В моем классе сущностей:

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

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

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

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

Ответ 4

Чтобы упростить ответ 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;
    }
}

Обеспечьте настройку своего пользовательского диалекта в сеансе конфигурации гибернации - factory список свойств:

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