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

ORA-12704: character set mismatch when performing multi-row INSERT of nullable NVARCHAR's

考虑以下 table,其中一列的类型可为空 NVARCHAR:

CREATE TABLE CHARACTER_SET_MISMATCH_TEST (
    ID NUMBER(10) NOT NULL,
    VALUE NVARCHAR2(32)
);

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

INSERT
    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)
    SELECT ?, ? FROM DUAL
    UNION ALL
    SELECT ?, ? FROM DUAL;

如果 NVARCHAR 值既是 NULL 又不是非 NULL,一切运行正常,我观察到恰好插入了 2 行。但是,如果我在单个 PreparedStatement 中混合 NULL 和非 NULL 值,我会立即收到 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)

这是重现问题的代码:

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));
        }
    }
}

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

,上述单元测试就可以正常通过
INSERT
    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)
    SELECT ?, TO_NCHAR(?) FROM DUAL
    UNION ALL
    SELECT ?, TO_NCHAR(?) FROM DUAL;

或切换到 INSERT ALL 语法:

INSERT ALL
    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)
    VALUES (?, ?)
    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)
    VALUES (?, ?)
    SELECT * FROM DUAL;

但是原始代码有什么问题?

推荐你三查

改这部分:

pstmt.setInt(1, 0);
pstmt.setNString(2, "NVARCHAR");
pstmt.setInt(3, 1);
pstmt.setNull(4, Types.NVARCHAR);

对此:

pstmt.setInt(1, 0);
pstmt.setString(2, "NVARCHAR");
pstmt.setInt(3, 1);
pstmt.setString(4, null);

(我认为这不是你的问题。它只是一个推荐,因为它可能会解决一些数据库字符集问题)

其次 检查你的连接池字符集:更喜欢设置"UTF-8"。 像这样的 spring.datasource.connectionProperties=useUnicode=true;characterEncoding=utf-8;

或者您可能是在应用程序服务器中设置它,或者您可能是在代码中处理它。

第三你必须用sql工具检查你的插入语句,比如plsql开发者或.. .并直接测试这条语句:

INSERT INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)
SELECT 1, 'test' FROM DUAL
UNION ALL
SELECT 2, null FROM DUAL;

甚至这个:

SELECT 1 aa, 'test' bb FROM DUAL
UNION ALL
SELECT 2 aa, null bb FROM DUAL;

如果您再次遇到错误。 这是因为 您的数据库字符集 与您的代码无关。

希望对您有所帮助。

您能否尝试使用以下 sql 代替:

SELECT ?, cast(? as nvarchar2(32)) FROM DUAL
UNION ALL
SELECT ?, cast(? as nvarchar2(32)) FROM DUAL;

我认为你的错误是因为默认情况下 null 是 varchar2 类型,并且你 sql 的所有部分的 union 类型不匹配。顺便说一句,检查你是否可以 运行 这个 sql 没有插入部分,看看错误是否仍然存在。

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

INSERT
    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)
    SELECT 0, 'abc' FROM DUAL
    UNION ALL
    SELECT 1, CAST(NULL AS NVARCHAR2(100)) FROM DUAL;
-- ORA-12704: character set mismatch

-- or
INSERT
INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)
SELECT 0, N'abc' FROM DUAL
UNION ALL
SELECT 1, CAST(NULL AS VARCHAR2(100)) FROM DUAL;
-- ORA-12704: character set mismatch

DBFiddle Demo


在 Oracle 中,如果您这样做:

SELECT N'abc' FROM dual
UNION ALL
SELECT 'abc' FROM dual

你会得到错误:

ORA-12704: character set mismatch

来自 UNION ALL doc:

If component queries select character data, then the datatype of the return values are determined as follows:

  • If both queries select values of datatype CHAR of equal length, then the returned values have datatype CHAR of that length. If the queries select values of CHAR with different lengths, then the returned value is VARCHAR2 with the length of the larger CHAR value.

  • If either or both of the queries select values of datatype VARCHAR2, then the returned values have datatype VARCHAR2.

所以回到你的工作方法:

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

INSERT
    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)
    SELECT ?, TO_NCHAR(?) FROM DUAL
    UNION ALL
    SELECT ?, TO_NCHAR(?) FROM DUAL;

2) 两个 "independent" INSERTs :

INSERT ALL
    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)
    VALUES (?, ?)
    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)
    VALUES (?, ?)
    SELECT * FROM DUAL;

3) "If NVARCHAR values are either both NULL or both non-NULL, everything runs fine and I observe exactly 2 rows inserted" - 相同的数据类型所以它工作正常

INSERT
    INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE)
    SELECT ?, ? FROM DUAL
    UNION ALL
    SELECT ?, ? FROM DUAL;

最后,如果存在 NULLNOT NULL 值,则会产生错误。它清楚地表明映射无效。我认为这与以下方面有关:

Valid SQL-JDBC Data Type Mappings:

┌────────────────────────┬──────────────────────────────────────────┐
│ These SQL data types:  │ Can be materialized as these Java types: │
├────────────────────────┼──────────────────────────────────────────┤
│ NVARCHAR2              │ no (see Note)                            │
└────────────────────────┴──────────────────────────────────────────┘

Note: The types NCHAR and NVARCHAR2 are supported indirectly. There is no corresponding java.sql.Types type, but if your application calls formOfUse(NCHAR), then these types can be accessed.

NCHAR, NVARCHAR2, NCLOB and the defaultNChar Property in JDK 1.5

By default, the oracle.jdbc.OraclePreparedStatement interface treats the data type of all the columns in the same way as they are encoded in the database character set. However, since Oracle Database 10g, if you set the value of oracle.jdbc.defaultNChar system property to true, then JDBC treats all character columns as being national-language.

The default value of defaultNChar is false. If the value of defaultNChar is false, then you must call the setFormOfUse(, OraclePreparedStatement.FORM_NCHAR) method for those columns that specifically need national-language characters.

所以你的 could 看起来像:

pstmt.setInt(1, 0);
pstmt.setFormOfUse(2, OraclePreparedStatement.FORM_NCHAR);
pstmt.setNString(2, "NVARCHAR");
pstmt.setInt(3, 1);
pstmt.setFormOfUse(4, OraclePreparedStatement.FORM_NCHAR);
pstmt.setNull(4, Types.NVARCHAR);

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

pstmt.setInt(1, 0);
pstmt.setNString(2, "NVARCHAR");
pstmt.setInt(3, 1);
pstmt.setNString(4, "");