La mia direzione e requisiti
- L'entità dovrebbe memorizzare XML come una stringa (java.lang.String)
- Il database deve persistere XML in una colonna XDB.XMLType
- Consente l'indicizzazione e query di tipo xpath/ExtractValue/xquery più efficienti
- Consolida una dozzina di soluzioni parziali che ho trovato nell'ultima settimana
- Ambiente di lavoro
- Oracle 11g r2 x64
- Iberna 4.1.x
- Java 1.7.x x64
- Windows 7 Pro x64
Soluzione dettagliata
Passaggio 1:trova xmlparserv2.jar (~1350 kb)
Questo jar è necessario per compilare il passaggio 2 ed è incluso nelle installazioni di Oracle qui:%ORACLE_11G_HOME%/LIB/xmlparserv2.jar
Passaggio 1.5:trova xdb6.jar (~257kb)
Questo è fondamentale se stai utilizzando Oracle 11gR2 11.2.0.2 o versioni successive o se stai archiviando come BINARY XML.
Perché?
- In 11.2.0.2+ la colonna XMLType viene archiviata utilizzando SECUREFILE BINARYXML per impostazione predefinita, mentre le versioni precedenti verranno archiviate come BASICFILECLOB
- Le versioni precedenti di xdb*.jar non decodificano correttamente l'xml binario e falliscono silenziosamente
- Driver JDBC di Google Oracle Database 11g Release 2 e scarica xdb6.jar
- Diagnosi e soluzione per il problema di decodifica XML binario descritto qui
Passaggio 2:crea un tipo utente ibernato per la colonna XMLType
Con Oracle 11g e Hibernate 4.x, è più facile di quanto sembri.
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) {}
}
}
}
Passaggio 3:annota il campo nella tua entità.
Sto usando le annotazioni con primavera/ibernazione, non i file di mappatura, ma immagino che la sintassi sarà simile.
@Type(type="your.custom.usertype.HibernateXMLType")
@Column(name="attribute_xml", columnDefinition="XDB.XMLTYPE")
private String attributeXml;
Passaggio 4:gestione degli errori di appserver/junit a seguito di Oracle JAR
Dopo aver incluso %ORACLE_11G_HOME%/LIB/xmlparserv2.jar (1350kb) nel tuo percorso di classe per risolvere gli errori di compilazione, ora ricevi errori di runtime dal tuo server delle applicazioni...
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 ...
PERCHÉ GLI ERRORI?
xmlparserv2.jar utilizza l'API dei servizi JAR (Service Provider Mechanism) per modificare le classi javax.xml predefinite utilizzate per SAXParserFactory, DocumentBuilderFactory e TransformerFactory.
COME È SUCCESSO?
javax.xml.parsers.FactoryFinder cerca le implementazioni personalizzate controllando, in questo ordine, le variabili di ambiente, %JAVA_HOME%/lib/jaxp.properties, quindi i file di configurazione in META-INF/services nel percorso di classe, prima di utilizzare il implementazioni predefinite incluse con JDK (com.sun.org.*).
All'interno di xmlparserv2.jar esiste una directory META-INF/services, che la classe javax.xml.parsers.FactoryFinder raccoglie. I file sono i seguenti:
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)
SOLUZIONE?
Ritorna tutti e 3, altrimenti vedrai strani errori.
- javax.xml.parsers.* corregge gli errori visibili
- javax.xml.transform.*corregge più sottili errori di analisi XML
- nel mio caso, con configurazione di Apache Commons leggere/scrivere
SOLUZIONE RAPIDA per risolvere gli errori di avvio del server delle applicazioni:argomenti JVM
Per ignorare le modifiche apportate da xmlparserv2.jar, aggiungere le seguenti proprietà JVM agli argomenti di avvio del server delle applicazioni. La logica java.xml.parsers.FactoryFinder verificherà prima le variabili di ambiente.
-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
Tuttavia, se esegui casi di test utilizzando @RunWith(SpringJUnit4ClassRunner.class) o simili, riscontrerai comunque l'errore.
MIGLIORE SOLUZIONE agli errori di avvio del server delle applicazioni E agli errori del test case? 2 opzioni
Opzione 1:usa gli argomenti JVM per il server delle app e le istruzioni @BeforeClass per i tuoi casi di test
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");
Se hai molti casi di test, questo diventa doloroso. Anche se lo metti in un super.
Opzione 2:crea i tuoi file di definizione del provider di servizi nel percorso di classe di compilazione/runtime per il tuo progetto, che sostituiranno quelli inclusi in xmlparserv2.jar
In un progetto primaverile esperto, sovrascrivi le impostazioni xmlparserv2.jar creando i seguenti file nella directory %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)
Questi file sono referenziati sia dal server delle applicazioni (nessun argomento JVM richiesto) e risolvono eventuali problemi di unit test senza richiedere modifiche al codice.
Fatto.