ORA-12704: несоответствие набора символов при выполнении многорядной вставки нулевых NVARCHAR

рассмотрим следующую таблицу, где один из столбцов имеет тип nullable NVARCHAR:

sql prettyprint-override">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 строки вставлены. Если, Однако, я смешиваю NULL и неNULL значения в пределах одного PreparedStatement, я немедленно получаю 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;

а что не так с исходным кодом?

3 ответов


если бы вы могли перехватить фактический запрос, который отправляется в БД, я думаю, он выглядит похожим на:

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


в 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 являются либо нулевыми, либо ненулевыми, все работает нормально, и я наблюдаю точно вставленные строки 2 " -тот же тип данных, поэтому он отлично работает

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

наконец-то случай где есть NULL и NOT NULL значение будет генерировать ошибку. Это ясно указывает на то, что сопоставление недопустимо. Я считаю, что это связано с:

допустимые сопоставления типов данных SQL-JDBC:

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

Примечание.: типы NCHAR и NVARCHAR2 поддерживаются косвенно. нет соответствующей java.язык SQL.Types type, но если ваше приложение вызывает formOfUse (NCHAR), то эти типы могут быть доступны.

и типа nchar, NVARCHAR2, NCLOB и свойство defaultNChar в JDK 1.5:

по умолчанию, oracle.интерфейс jdbc.Интерфейс OraclePreparedStatement обрабатывает тип данных всех столбцов так же, как они кодируются в наборе символов базы данных. Однако, начиная с Oracle Database 10g, если вы установите значение oracle.интерфейс jdbc.системное свойство defaultNChar имеет значение true, затем JDBC обрабатывает все символьные столбцы как национальные.

значение по умолчанию defaultNChar является ложным. Если значение 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, "");

можете ли вы попробовать использовать следующий sql вместо этого:

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

Я думаю, что ваша ошибка, потому что null по умолчанию является типом varchar2, и есть несоответствие типов в union всей части вашего sql. Кстати, чтобы проверить, что вы можете запустить этот sql без вставки части и посмотреть, если ошибка все еще выходит или нет.


Я рекомендую вам три чека.

первый изменить эту часть:

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". что-то вроде этого весна.источник.connectionProperties=useUnicode=true; characterEncoding=utf-8;

или может быть, вы установите его на сервере приложений или можете обрабатывать его в коде.

третий вы должны Проверьте инструкцию insert с помощью sql tools как разработчик 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;

если вы снова получили ошибку. потому что ваш набор символов базы данных и не связано с вашим кодом.

Я надеюсь, что это поможет.