Oracle
 sql >> Base de Dados >  >> RDS >> Oracle

Usando a coluna Oracle XMLType em hibernação


Minha direção e requisitos
  • A entidade deve armazenar XML como uma string (java.lang.String)
  • O banco de dados deve manter o XML em uma coluna XDB.XMLType
    • Permite indexação e consultas de tipo xpath/ExtractValue/xquery mais eficientes
  • Consolide cerca de uma dúzia de soluções parciais que encontrei na última semana
  • Ambiente de Trabalho
    • Oracle 11g r2 x64
    • Hibernar 4.1.x
    • Java 1.7.x x64
    • Windows 7 Pro x64

Solução passo a passo

Etapa 1:encontre xmlparserv2.jar (~1350kb)

Este jar é necessário para compilar a etapa 2 e está incluído nas instalações do oracle aqui:%ORACLE_11G_HOME%/LIB/xmlparserv2.jar

Etapa 1.5:encontre xdb6.jar (~257kb)

Isso é crítico se você estiver usando o Oracle 11gR2 11.2.0.2 ou superior, ou armazenando como XML BINARY.

Por quê?
  • Na versão 11.2.0.2+, a coluna XMLType é armazenada usando SECUREFILE BINARYXML por padrão, enquanto as versões anteriores serão armazenadas como um BASICFILECLOB
  • Versões mais antigas do xdb*.jar não decodificam corretamente o xml binário e falham silenciosamente
    • Google Drivers JDBC do Oracle Database 11g versão 2 e baixe xdb6.jar
  • Diagnóstico e solução para o problema de decodificação de XML binário descrito aqui

Etapa 2:crie um UserType de hibernação para a coluna XMLType

Com Oracle 11g e Hibernate 4.x, isso é mais fácil do que parece.
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) {}
        }
    }
}   

Etapa 3:anote o campo em sua entidade.

Estou usando anotações com spring/hibernate, não mapeando arquivos, mas imagino que a sintaxe seja semelhante.
@Type(type="your.custom.usertype.HibernateXMLType")
@Column(name="attribute_xml", columnDefinition="XDB.XMLTYPE")
private String attributeXml;

Etapa 4:Lidando com os erros appserver/junit como resultado do Oracle JAR

Depois de incluir %ORACLE_11G_HOME%/LIB/xmlparserv2.jar (1350kb) em seu classpath para resolver erros de compilação, agora você obtém erros de tempo de execução do seu servidor de aplicativos...
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 ...

POR QUE OS ERROS?

O xmlparserv2.jar usa a API de Serviços JAR (Mecanismo do Provedor de Serviços) para alterar as classes javax.xml padrão usadas para SAXParserFactory, DocumentBuilderFactory e TransformerFactory.

COMO ACONTECEU?

O javax.xml.parsers.FactoryFinder procura implementações customizadas verificando, nesta ordem, variáveis ​​de ambiente, %JAVA_HOME%/lib/jaxp.properties, então para arquivos de configuração em META-INF/services no caminho de classe, antes de usar o implementações padrão incluídas no JDK (com.sun.org.*).

Dentro do xmlparserv2.jar existe um diretório META-INF/services, que a classe javax.xml.parsers.FactoryFinder pega. Os arquivos são os seguintes:
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)

SOLUÇÃO?

Alterne todos os 3 de volta, caso contrário, você verá erros estranhos.
  • javax.xml.parsers.* corrija os erros visíveis
  • javax.xml.transform.*corrige erros de análise de XML mais sutis
    • no meu caso, com configuração do Apache Commons ler/escrever

SOLUÇÃO RÁPIDA para resolver os erros de inicialização do servidor de aplicativos:Argumentos da JVM

Para substituir as alterações feitas por xmlparserv2.jar, inclua as seguintes propriedades da JVM em seus argumentos de inicialização do servidor de aplicativos. A lógica java.xml.parsers.FactoryFinder verificará as variáveis ​​de ambiente primeiro.
-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

No entanto, se você executar casos de teste usando @RunWith(SpringJUnit4ClassRunner.class) ou similar, ainda ocorrerá o erro.

MELHOR SOLUÇÃO para os erros de inicialização do servidor de aplicativos E erros de casos de teste? 2 opções

Opção 1:use argumentos JVM para o servidor de aplicativos e instruções @BeforeClass para seus casos de teste
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 você tiver muitos casos de teste, isso se tornará doloroso. Mesmo se você colocá-lo em um super.

Opção 2:Crie seus próprios arquivos de definição de Provedor de Serviços no caminho de classe de compilação/tempo de execução para seu projeto, que substituirá aqueles incluídos em xmlparserv2.jar

Em um projeto maven spring, substitua as configurações de xmlparserv2.jar criando os seguintes arquivos no diretório %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)

Esses arquivos são referenciados pelo servidor de aplicativos (sem argumentos JVM necessários) e resolvem quaisquer problemas de teste de unidade sem exigir nenhuma alteração de código.

Feito.