在hibernate中使用Oracle XMLType列

a1e*_*x07 28 xml oracle hibernate

我需要将Oracle XMLType列映射到hibernate实体类.有一个工作(我认为众所周知)的解决方案涉及实施UserType; 但是,我无法使用它,因为需要导入Oracle xml解析器,这反过来会导致许多问题.
我可以将xml列的值作为字符串访问并将转换保留给操作实体的代码,但是我找不到从中读取值并将其写入数据库的方法.到目前为止我尝试了什么:

  1. 将实体类中的属性声明为String.结果 - 值读为null.如果属性是公正的Serializable,我会得到"无法反序列化"的异常.
  2. 使用@Formula注释(CAST xmlCol as varchar2(1000)).结果 - 不存储值
  3. 使用@Loader并把CASTSELECT.这是最有希望的尝试 - 读取并成功存储了值,但是当加载包含xml列的实体的集合时,我得到null(@Loader如果基础表被LEFT JOIN编辑,Hibernate不使用sql ).

我认为应该工作的另一种方法是将xml列作为String(用于写入)加上用于读取的虚拟字段@Formula; 然而,它看起来像是一个肮脏的黑客,我宁愿不这样做,除非我别无选择.

最后,我能做的最后一件事就是更改DB Schema(更多的是1选项,比如view + triggers,列数据类型更改),但这对我来说也不是一个好选择.

我想知道我是否遗漏了某些东西或者是否有办法让(3)工作?

Mat*_*t M 13

我的方向和要求

  • 实体应将XML存储为字符串(java.lang.String)
  • 数据库应该在XDB.XMLType列中保留XML
    • 允许索引和更高效的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需要此jar,并在此处包含在oracle安装中:%ORACLE_11G_HOME%/ LIB/xmlparserv2.jar

步骤1.5:查找xdb6.jar(~257kb)

如果您使用的是Oracle 11gR2 11.2.0.2或更高版本,或者存储为BINARY XML,这一点至关重要.

为什么?

第2步:为XMLType列创建一个hibernate UserType

使用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) {}
        }
    }
}   
Run Code Online (Sandbox Code Playgroud)

第3步:注释实体中的字段.

我正在使用带有spring/hibernate的注释,而不是映射文件,但我认为语法将类似.

@Type(type="your.custom.usertype.HibernateXMLType")
@Column(name="attribute_xml", columnDefinition="XDB.XMLTYPE")
private String attributeXml;
Run Code Online (Sandbox Code Playgroud)

第4步:处理Oracle JAR导致的appserver/junit错误

在类路径中包含%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 ...
Run Code Online (Sandbox Code Playgroud)

为什么错误?

xmlparserv2.jar使用JAR服务API(服务提供程序机制)来更改用于SAXParserFactory,DocumentBuilderFactory和TransformerFactory的缺省javax.xml类.

这是怎么发生的?

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)
Run Code Online (Sandbox Code Playgroud)

解?

切换所有3,否则你会看到奇怪的错误.

  • javax.xml.parsers.*修复了可见错误
  • javax.xml.transform.*修复了更微妙的XML解析错误
    • 在我的情况下,用apache commons配置读/写

QUICK SOLUTION解决应用程序服务器启动错误: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
Run Code Online (Sandbox Code Playgroud)

但是,如果使用@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");
Run Code Online (Sandbox Code Playgroud)

如果你有很多测试用例,那就太痛苦了.即使你把它放在超级.

选项2:在项目的编译/运行时类路径中创建自己的服务提供者定义文件,这将覆盖xmlparserv2.jar中包含的那些文件.

在maven spring项目中,通过在%PROJECT_HOME%/ src/main/resources目录中创建以下文件来覆盖xmlparserv2.jar设置:

%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)
Run Code Online (Sandbox Code Playgroud)

这些文件都由应用程序服务器引用(不需要JVM参数),并且无需任何代码更改即可解决任何单元测试问题.

完成.


小智 6

为了进一步简化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;
Run Code Online (Sandbox Code Playgroud)

您可能还必须在自定义方言中将clob注册为xmltype.如此有效,您将拥有以下内容:

public class OracleDialectExtension extends org.hibernate.dialect.Oracle10gDialect {
    public OracleDialectExtension() {
        super();
        registerColumnType(Types.CLOB, "xmltype");
    }

    @Override
    public boolean useInputStreamToInsertBlob() {
        return false;
    }
}
Run Code Online (Sandbox Code Playgroud)

确保在hibernate配置的会话工厂属性列表中设置自定义方言:

<property name="hibernate.dialect"><!-- class path to custom dialect class --></property>
Run Code Online (Sandbox Code Playgroud)


小智 5

对此存在更简单的解决方案.只需使用ColumnTransformer Annotation.

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


Cel*_*lso 5

在尝试了许多不同的方法而没有运气之后,我想出了这个:

在我的实体类上:

@ColumnTransformer(read = "NVL2(EVENT_DETAILS, (EVENT_DETAILS).getClobVal(), NULL)", write = "NULLSAFE_XMLTYPE(?)")
@Lob
@Column(name="EVENT_DETAILS")
private String details;
Run Code Online (Sandbox Code Playgroud)

请注意"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;
Run Code Online (Sandbox Code Playgroud)

在我的persistence.xml文件中:

<property name="hibernate.dialect" value="mypackage.CustomOracle10gDialect" />
Run Code Online (Sandbox Code Playgroud)

自定义方言(如果我们不覆盖"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;
    }
}
Run Code Online (Sandbox Code Playgroud)

这对我来说非常适合使用Hibernate 4.3.6和Oracle 11.2.0.1.0(使用ojdbc6-11.1.0.7.0.jar).

我不得不承认我没有尝试过Matt M的解决方案,因为它涉及大量的黑客攻击和使用不在标准Maven存储库中的库.

Kamuffel的解决方案是我的出发点,但是当我尝试插入大型XML时出现了ORA-01461错误,这就是为什么我必须创建自己的方言.此外,我发现TO_CLOB(XML_COLUMN)方法存在问题(我会得到"ORA-19011:字符串缓冲区太小"错误).我猜这种方式首先将XMLTYPE值转换为VARCHAR2然后再转换为CLOB,从而在尝试读取大型XML时导致问题.这就是为什么经过一些研究后我决定使用XML_COLUMN.getClobVal()代替.

我没有在互联网上找到这个确切的解决方案.这就是为什么我决定创建一个StackOverflow帐户来发布它,以防它可能对其他人有帮助.

我正在使用JAXB来构造XML String,但我认为在这种情况下它并不相关.