ORA-12704:执行可以为空的NVARCHAR的多行INSERT时字符集不匹配

Bas*_*ass 12 java sql oracle nvarchar jdbc

请考虑下表,其中一列的类型为nullable NVARCHAR:

CREATE TABLE CHARACTER_SET_MISMATCH_TEST (
    ID NUMBER(10) NOT NULL,
    VALUE NVARCHAR2(32)
);
Run Code Online (Sandbox Code Playgroud)

现在,我想使用多行INSERT(带子查询)语法将多个数据元组插入到此表中:

INSERT
    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)
    SELECT ?, ? FROM DUAL
    UNION ALL
    SELECT ?, ? FROM DUAL;
Run Code Online (Sandbox Code Playgroud)

如果NVARCHAR值是两者NULL都是非NULL,或者两者都是非的,那么一切都运行正常,我确实插入了2行.但是,如果我在一个单独的混合NULL和非NULLPreparedStatement,我立即收到一个ORA-12704: character set mismatch错误:

java.sql.SQLException: ORA-12704: character set mismatch
    at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:452)
    at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:400)
    at oracle.jdbc.driver.T4C8Oall.processError(T4C8Oall.java:884)
    at oracle.jdbc.driver.T4CTTIfun.receive(T4CTTIfun.java:471)
    at oracle.jdbc.driver.T4CTTIfun.doRPC(T4CTTIfun.java:199)
    at oracle.jdbc.driver.T4C8Oall.doOALL(T4C8Oall.java:535)
    at oracle.jdbc.driver.T4CPreparedStatement.doOall8(T4CPreparedStatement.java:238)
    at oracle.jdbc.driver.T4CPreparedStatement.executeForRows(T4CPreparedStatement.java:1385)
    at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout(OracleStatement.java:1709)
    at oracle.jdbc.driver.OraclePreparedStatement.executeInternal(OraclePreparedStatement.java:4364)
    at oracle.jdbc.driver.OraclePreparedStatement.executeUpdate(OraclePreparedStatement.java:4531)
    at oracle.jdbc.driver.OraclePreparedStatementWrapper.executeUpdate(OraclePreparedStatementWrapper.java:5575)
Run Code Online (Sandbox Code Playgroud)

这是重现问题的代码:

package com.example;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Types;

import javax.sql.DataSource;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import oracle.jdbc.pool.OracleConnectionPoolDataSource;
import oracle.jdbc.pool.OracleDataSource;

public final class Ora12704Test {
    @NonNull
    private static final String SQL = "INSERT INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE) SELECT ?, ? FROM DUAL UNION ALL SELECT ?, ? FROM DUAL";

    @Nullable
    private static DataSource dataSource;

    @Nullable
    private Connection conn;

    @BeforeClass
    public static void setUpOnce() throws SQLException {
        dataSource = new OracleConnectionPoolDataSource();
        ((OracleDataSource) dataSource).setURL("jdbc:oracle:thin:@:1521:XE");
    }

    @BeforeMethod
    public void setUp() throws SQLException {
        this.conn = dataSource.getConnection("SANDBOX", "SANDBOX");
    }

    @AfterMethod
    public void tearDown() throws SQLException {
        if (this.conn != null) {
            this.conn.close();
        }
        this.conn = null;
    }

    @Test
    public void testNullableNvarchar()
    throws SQLException {
        try (final PreparedStatement pstmt = this.conn.prepareStatement(SQL)) {
            pstmt.setInt(1, 0);
            pstmt.setNString(2, "NVARCHAR");
            pstmt.setInt(3, 1);
            pstmt.setNull(4, Types.NVARCHAR);

            final int rowCount = pstmt.executeUpdate();
            assertThat(rowCount, is(2));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

奇怪的是,如果我明确地将我的参数转换为NCHAR:

INSERT
    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)
    SELECT ?, TO_NCHAR(?) FROM DUAL
    UNION ALL
    SELECT ?, TO_NCHAR(?) FROM DUAL;
Run Code Online (Sandbox Code Playgroud)

或切换到INSERT ALL语法:

INSERT ALL
    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)
    VALUES (?, ?)
    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)
    VALUES (?, ?)
    SELECT * FROM DUAL;
Run Code Online (Sandbox Code Playgroud)

但原始代码有什么问题?

Luk*_*zda 2

如果您可以拦截发送到数据库的实际查询,我想它看起来类似于:

\n\n
INSERT\n    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)\n    SELECT 0, \'abc\' FROM DUAL\n    UNION ALL\n    SELECT 1, CAST(NULL AS NVARCHAR2(100)) FROM DUAL;\n-- ORA-12704: character set mismatch\n\n-- or\nINSERT\nINTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)\nSELECT 0, N\'abc\' FROM DUAL\nUNION ALL\nSELECT 1, CAST(NULL AS VARCHAR2(100)) FROM DUAL;\n-- ORA-12704: character set mismatch\n
Run Code Online (Sandbox Code Playgroud)\n\n

DBFiddle 演示

\n\n
\n\n

在 Oracle 中,如果您这样做:

\n\n
SELECT N\'abc\' FROM dual\nUNION ALL\nSELECT \'abc\' FROM dual\n
Run Code Online (Sandbox Code Playgroud)\n\n

你会得到错误:

\n\n
\n

ORA-12704: 字符集不匹配

\n
\n\n

UNION ALL doc

\n\n
\n

如果组件查询选择字符数据,则返回值的数据类型确定如下:

\n\n
    \n
  • 如果两个查询都选择长度相等的 CHAR 数据类型的值,则返回的值具有该长度的 CHAR 数据类型。如果查询选择不同长度的 CHAR 值,则返回值为 VARCHAR2,其长度为较大的 CHAR 值。

  • \n
  • 如果其中一个或两个查询选择数据类型 VARCHAR2 的值,则返回的值的数据类型为 VARCHAR2。

  • \n
\n
\n\n

那么回到你的工作方法:

\n\n

1)相同的数据类型(显式转换)

\n\n
INSERT\n    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)\n    SELECT ?, TO_NCHAR(?) FROM DUAL\n    UNION ALL\n    SELECT ?, TO_NCHAR(?) FROM DUAL;\n
Run Code Online (Sandbox Code Playgroud)\n\n

2)两个“独立” INSERTs

\n\n
INSERT ALL\n    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)\n    VALUES (?, ?)\n    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)\n    VALUES (?, ?)\n    SELECT * FROM DUAL;\n
Run Code Online (Sandbox Code Playgroud)\n\n

3)“如果 NVARCHAR 值都是 NULL 或都是非 NULL,则一切运行正常,并且我观察到插入了 2 行” - 相同的数据类型,因此工作正常

\n\n
INSERT\n    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)\n    SELECT ?, ? FROM DUAL\n    UNION ALL\n    SELECT ?, ? FROM DUAL;\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

NULL最后,有值的情况NOT NULL会产生错误。它清楚地表明映射无效。我相信这与:

\n\n

有效的 SQL-JDBC 数据类型映射

\n\n
\n
\xe2\x94\x8c\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\xac\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x90\n\xe2\x94\x82 These SQL data types:  \xe2\x94\x82 Can be materialized as these Java types: \xe2\x94\x82\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\xbc\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\xa4\n\xe2\x94\x82 NVARCHAR2              \xe2\x94\x82 no (see Note)                            \xe2\x94\x82\n\xe2\x94\x94\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\xb4\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x98\n
Run Code Online (Sandbox Code Playgroud)\n\n

注意:\n 间接支持 NCHAR 和 NVARCHAR2 类型。没有相应的 java.sql.Types 类型,但如果您的应用程序调用formOfUse(NCHAR),则可以访问这些类型。

\n
\n\n

以及JDK 1.5 中的 NCHAR、NVARCHAR2、NCLOB 和 defaultNChar 属性

\n\n
\n

默认情况下,oracle.jdbc.OraclePreparedStatement 接口以与数据库字符集中编码相同的方式处理所有列的数据类型。但是,从 Oracle Database 10g 开始,如果将 oracle.jdbc.defaultNChar 系统属性的值设置为 true,则 JDBC 会将所有字符列视为国家语言。

\n\n

defaultNChar 的默认值为 false。如果 defaultNChar 的值为 false,则必须为那些特别需要本地语言字符的列调用 setFormOfUse(, OraclePreparedStatement.FORM_NCHAR) 方法。

\n
\n\n

所以你的可能看起来像:

\n\n
pstmt.setInt(1, 0);\npstmt.setFormOfUse(2, OraclePreparedStatement.FORM_NCHAR);\npstmt.setNString(2, "NVARCHAR");\npstmt.setInt(3, 1);\npstmt.setFormOfUse(4, OraclePreparedStatement.FORM_NCHAR);\npstmt.setNull(4, Types.NVARCHAR);\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

另一种想法:Oracle 将空字符串视为与NULL下面的代码相同,因此也应该可以正常工作:

\n\n
pstmt.setInt(1, 0);\npstmt.setNString(2, "NVARCHAR");\npstmt.setInt(3, 1);\npstmt.setNString(4, "");\n
Run Code Online (Sandbox Code Playgroud)\n