Использование столбца Oracle XMLType в спящем режиме
Мне нужно сопоставить столбец Oracle XMLType с классом объектов гибернации. Существует рабочее (и я думаю, хорошо известное) решение, которое включает в себя реализацию UserType
; однако я не могу использовать его, потому что требует импорта парсеров Oracle xml, что, в свою очередь, вызывает много проблем.
Я в порядке доступа к значению столбца xml в виде строки и оставляю преобразование в коде, который управляет сущностью, но я не могу найти способ прочитать значение и записать его в базу данных. Что я уже пробовал:
- Объявление свойства в классе сущности как
String
, Результат - значение читается какnull
, Если собственность простоSerializable
Я получаю исключение "не могу десериализовать". - С помощью
@Formula
аннотация (CAST xmlCol as varchar2(1000)
). Результат - значение не сохраняется - С помощью
@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.
Зачем?
- В 11.2.0.2+ столбец XMLType хранится с использованием SECUREFILE BINARY XML по умолчанию, тогда как более ранние версии будут храниться как BASICFILE CLOB.
- Более старые версии xdb*.jar неправильно декодируют двоичный файл xml и молча терпят неудачу
- Google Oracle Database 11g, выпуск 2, драйверы JDBC и загрузка xdb6.jar
- Диагностика и решение проблемы двоичного 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 как временную зависимость. Таким образом, мне не нужно возиться с конфигурацией сервера приложений, и модульные тесты работают нормально.