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

Posted

技术标签:

【中文标题】ORA-12704: 执行可为空的 NVARCHAR 的多行 INSERT 时字符集不匹配【英文标题】:ORA-12704: character set mismatch when performing multi-row INSERT of nullable NVARCHAR's 【发布时间】:2017-07-20 16:03:59 【问题描述】:

考虑下表,其中一列的类型为可为空的NVARCHAR

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

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

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;

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

【问题讨论】:

【参考方案1】:

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

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: 字符集不匹配

来自UNION ALL doc

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

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

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

所以回到你的工作方法:

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

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

2) 两个“独立”INSERTs

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

3) “如果 NVARCHAR 值既为 NULL 也为非 NULL,则一切运行正常,我观察到正好插入了 2 行”- 相同的数据类型,所以它工作正常

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)                            │
└────────────────────────┴──────────────────────────────────────────┘

注意: 间接支持NCHAR和NVARCHAR2类型。没有对应的java.sql.Types类型,但是如果你的应用调用formOfUse(NCHAR),那么这些类型是可以访问的.

还有NCHAR, NVARCHAR2, NCLOB and the defaultNChar Property in JDK 1.5:

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

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

所以你的可能看起来像:

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, "");

【讨论】:

【参考方案2】:

您可以尝试使用以下 sql 代替吗:

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

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

【讨论】:

【参考方案3】:

我建议你三个检查。

首先改变这部分:

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 developer 或 ... 并直接测试此语句:

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;

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

希望对你有所帮助。

【讨论】:

不幸的是,这甚至不能很好地回答我的问题。 1 由于我希望我的 JDBC 代码保持与数据库无关,并且由于 OracleVARCHAR2NVARCHAR2 类型的处理方式不同,因此我无法将 setNString() 调用替换为setString() 或反之亦然。 2 问题出在原始物理连接上,所以与连接池无关。 3 AL16UTF16 用于NVARCHAR 的,而 Oracle 在这里没有提供很多替代方案,因此您对数据库字符集的评论毫无意义。投反对票。 你在plsql或sqldeveloper中测试过查询吗? 特别是没有插入的选择? 我认为 setNString 的使用有所不同。请检查此***.com/questions/49920395/jdbc-getnstring 您可以使用 varchar2 和 nvarchar2 设置字符串 某些数据库可以(mysql)或肯定会(MS SQL Server)使用ANSI(非Unicode)编码来存储常规VARCHAR的(相对于NVARCHAR 的)。相应的 JDBC 驱动程序的行为可能类似。因此,setNString() 不能完全替换为setString()

以上是关于ORA-12704: 执行可为空的 NVARCHAR 的多行 INSERT 时字符集不匹配的主要内容,如果未能解决你的问题,请参考以下文章

将 int.TryParse 与可为空的 int 一起使用 [重复]

对 Oracle 的 EF 查询抛出“ORA-12704:字符集不匹配”

将可为空的 int 映射到可为空的 int + automapper

在可为空的十进制属性上使用 Math.Abs

ORA-12704: 字符集不匹配

检查可为空的布尔值是不是为空[重复]