【问题标题】:Using Oracle XMLType column in hibernate在休眠中使用 Oracle XMLType 列
【发布时间】:2012-07-19 16:36:03
【问题描述】:

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

  1. 将实体类中的属性声明为String。结果 - 值被读取为null。如果属性只是 Serializable,我会得到“无法反序列化”异常。
  2. 使用@Formula 注释(CAST xmlCol as varchar2(1000))。结果 - 值未存储
  3. 使用@Loader 并将CAST 放入SELECT。这是最有希望的尝试 - 值已成功读取和存储,但在加载包含 xml 列的实体集合时,我得到 null(如果基础表为 @987654332,Hibernate 不会在 @Loader 中使用 sql @ed)。

我认为应该可行的另一种方法是将 xml 列设置为String(用于写入)加上虚拟字段用于读取@Formula;但是,这对我来说似乎是一种肮脏的黑客行为,除非我别无选择,否则我宁愿不这样做。

最后,我能做的最后一件事是更改 DB Schema(还有不止 1 个选项,例如视图 + 触发器、列数据类型更改),但这对我来说也不是一个好选择。

我想知道我是否遗漏了什么,或者是否有办法让 (3) 起作用?

【问题讨论】:

  • 您是否尝试将XMLType 视为CLOB
  • 我做到了。它抛出一个异常。
  • @Tarion:不,我终于放弃了......除了使用自定义类型(由于解析器问题我不能这样做)我没有找到任何其他可靠的解决方案。
  • 所以我的解决方案是使用 CLOB,然后为它取消一个简单的 String getter 和 setter。效果很好,无论如何我都会在我的 java 代码中解组 xml。
  • @Tarion:是的,它有效;我最终做了同样的事情,但我希望 Oracle 进行 xml 验证......使用 CLOB 的解决方案不会在数据库级别强制执行任何操作。

标签: xml oracle hibernate


【解决方案1】:

我的方向和要求

  • 实体应将 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 (~1350kb)

此 jar 是编译步骤 2 所必需的,并且包含在此处的 oracle 安装中: %ORACLE_11G_HOME%/LIB/xmlparserv2.jar

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

如果您使用 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
  • Diagnosis and solution for Binary XML decoding problem outlined here

第 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 步:处理由 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 ...

为什么会出现错误?

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)

解决方案?

将 3 个全部切换回来,否则您会看到奇怪的错误。

  • javax.xml.parsers.* 修复可见错误
  • javax.xml.transform.* 修复更细微的 XML 解析错误
    • 就我而言,使用 apache commons 配置 读/写

解决应用服务器启动错误的快速解决方案:JVM Arguments

要覆盖 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");

如果你有很多测试用例,这会变得很痛苦。即使你把它放在一个super中。

选项 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)

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

完成。

【讨论】:

  • 这个答案有最好的解释(与 Oracle XMLTYPE 解析相关),我可以在互联网上找到,快速解决方案适用于我的问题。就我而言,我在 tomcat7.0/lib 文件夹中有 xdb.jar 和 xmlparserv2.jar(都来自 Oracle 11g 安装文件夹),这在启动时给了我this warning。伙计,你摇滚!
  • 已更新以包含与通过 java 从存储为 BINARY XML 的 XMLType 列中检索数据相关的潜在问题/解决方案,这是从 Oracle 11gR2 v11.2.0.2+ 开始的默认设置。
【解决方案2】:

有一个更简单的解决方案。只需使用 ColumnTransformer 注释即可。

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

【讨论】:

  • 如果存储为 CLOB,还可以通过 SQL 进行 xpath 查询吗?
  • 如果 xml-data 大于 4000 个字符,您会收到 ORA 错误
【解决方案3】:

在尝试了许多不同的方法后,我想出了这个:

在我的实体类上:

@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: can bind a LONG value only for insert into a LONG column”错误):

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 存储库中不存在的库。

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

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

我正在使用 JAXB 来构造 XML 字符串,但我认为在这种情况下它不相关。

【讨论】:

    【解决方案4】:

    为了进一步简化 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;
        }
    }
    

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

    <property name="hibernate.dialect"><!-- class path to custom dialect class --></property>
    

    【讨论】:

      【解决方案5】:

      我从 Hibernate 3.6.* 迁移到 Hibernate 5.4 时遇到了问题,我通过在 Oracle xmlparserv2 之前添加 dbUnit maven 依赖项解决了这个问题。 dbUnit 具有 xerces:xercesImpl 作为瞬态依赖项。这样我就不必弄乱 App Server Config 并且单元测试运行得很好。

      【讨论】:

        猜你喜欢
        • 2011-07-16
        • 2018-11-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-02-08
        • 2014-03-27
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多