Моите насоки и изисквания
- Обектът трябва да съхранява XML като низ (java.lang.String)
- База данни трябва да запази XML в колона XDB.XMLType
- Позволява индексиране и по-ефективни заявки от типа xpath/ExtractValue/xquery
- Консолидиране на дузина частични решения, които намерих през последната седмица
- Работна среда
- Oracle 11g r2 x64
- Хибернация 4.1.x
- Java 1.7.x x64
- Windows 7 Pro x64
Решение стъпка по стъпка
Стъпка 1:Намерете xmlparserv2.jar (~1350 kb)
Този jar е необходим за компилиране на стъпка 2 и е включен в инсталациите на oracle тук:%ORACLE_11G_HOME%/LIB/xmlparserv2.jar
Стъпка 1.5:Намерете xdb6.jar (~257 kb)
Това е от решаващо значение, ако използвате Oracle 11gR2 11.2.0.2 или по-нова версия или съхранявате като BINARY XML.
Защо?
- В 11.2.0.2+ колоната XMLType се съхранява с помощта на SECUREFILE BINARYXML по подразбиране, докато по-ранните версии ще се съхраняват като BASICFILECLOB
- По-старите версии на xdb*.jar не декодират правилно двоичен xml и се провалят безшумно
- Google Oracle Database 11g Release 2 JDBC Drivers и изтеглете 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 използва 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)
РЕШЕНИЕ?
Превключете всичките 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) и решава всички проблеми с тестовете на модула, без да изисква промени в кода.
Готово.