ORA-12704: несоответствие набора символов при выполнении многострочного INSERT с нулевыми значениями NVARCHAR
Рассмотрим следующую таблицу, в которой один из столбцов имеет тип 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
либо оба non- NULL
, все работает нормально, и я наблюдаю ровно 2 строки. Если, однако, я смешиваю значения NULL
и non- 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));
}
}
}
Как ни странно, вышеупомянутый unit тест проходит просто отлично, если я явно NCHAR
свои параметры в 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
В 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;
Наконец, случай, когда 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, но если ваше приложение вызывает formOfUse (NCHAR), к ним можно получить доступ к этим типам.
И NCHAR, NVARCHAR2, NCLOB и свойство defaultNChar в JDK 1.5:
По умолчанию интерфейс oracle.jdbc.OraclePreparedStatement обрабатывает тип данных всех столбцов так же, как они закодированы в наборе символов базы данных. Однако, поскольку Oracle Database 10g, если вы установили значение системного свойства oracle.jdbc.defaultNChar в значение true, JDBC обрабатывает все столбцы символов как национальные.
Значение по умолчаниюNChar по умолчанию - false. Если значение defaultNChar является ложным, вы должны вызвать метод 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. Btw, чтобы проверить, что вы можете запустить этот 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;
или, может быть, вы установите его на сервере приложений или, возможно, будете обрабатывать его в коде.
В-третьих, вы должны проверить свою инструкцию insert с помощью 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;
Если вы снова получили ошибку. потому что ваш набор символов базы данных и не связан с вашим кодом.
Надеюсь, это поможет.