12

Consider the following table where one of the columns is of type nullable NVARCHAR:

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

Now, I want to insert multiple data tuples into this table using the multi-row INSERT (with sub-query) syntax:

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

If NVARCHAR values are either both NULL or both non-NULL, everything runs fine and I observe exactly 2 rows inserted. If, however, I mix NULL and non-NULL values within a single PreparedStatement, I immediately receive an ORA-12704: character set mismatch error:

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)

Here's the code which reproduces the issue:

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

Strangely, the above unit test passes just fine if I explicitly cast my parameters to NCHAR:

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

or switch to the INSERT ALL syntax:

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

But what's wrong with the original code?

Bass
  • 4,977
  • 2
  • 36
  • 82

3 Answers3

2

If you could intercept actual query that is sent to DB I guess it looks similiar to:

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


In Oracle if you do:

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

You will get error:

ORA-12704: character set mismatch

From 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.

So returning to your working approaches:

1) Same data type(explicit conversion)

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

2) Two "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" - same data type so it works fine

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

Finally case where there is NULL and NOT NULL value will generate error. It clearly indicates that mapping is not valid. I believe it is related to:

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.

And 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.

So your could will look like:

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

One more thought: Oracle treats empty string same as NULL so below code should also work fine:

pstmt.setInt(1, 0);
pstmt.setNString(2, "NVARCHAR");
pstmt.setInt(3, 1);
pstmt.setNString(4, "");
Lukasz Szozda
  • 162,964
  • 23
  • 234
  • 275
1

Can you try to use following sql instead:

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

I think your error because null by default is varchar2 type and there is type mismatch in union all part of your sql. Btw to check that you can run this sql without insert part and see if error still exits or not.

Rusty
  • 1,988
  • 10
  • 12
-1

I recommend you three check.

First change this part:

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

to this:

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

(I think its not your problem. its only a recommend because it may solve some database character set problem)

Second check your connection pool character set: prefer to set "UTF-8". something like this spring.datasource.connectionProperties=useUnicode=true;characterEncoding=utf-8;

or may be you set it in application server or may be you handle it in the code.

Third you must check your insert statement with sql tools like plsql developer or ... and test this statement directly:

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

or even this:

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

If you got the error again. its because your database character set and not related to your code.

I hope this help.

  • Unfortunately, this is not even closely an answer to my question. **1** Since I want my JDBC code to remain database agnostic, and since _Oracle_ treats `VARCHAR2` and `NVARCHAR2` types differently, I can't replace `setNString()` calls with `setString()` or vice versa. **2** The problem occurs with the raw physical connection, so connection pool has nothing to do with it. **3** `AL16UTF16` is used for `NVARCHAR`'s, and _Oracle_ doesn't provide many alternatives here, so your remark about database character set is pointless. Downvoting. – Bass Jul 18 '18 at 11:26
  • Did you test the query in plsql or sqldeveloper? – mehran monster Jul 18 '18 at 17:13
  • Specially the select without insert? – mehran monster Jul 18 '18 at 17:13
  • I think different about using of setNString. Pls check this https://stackoverflow.com/questions/49920395/jdbc-getnstring you can use both varchar2 and nvarchar2 with set string – mehran monster Jul 18 '18 at 17:22
  • Certain databases can (_MySQL_) or definitely will (_MS SQL Server_) use ANSI (non-Unicode) encodings to store regular `VARCHAR`'s (as opposed to `NVARCHAR`'s). The corresponding JDBC drivers may behave similarly. Therefore, `setNString()` can't be fully replaced with `setString()`. – Bass Jul 18 '18 at 19:19
  • I think that this can help you to recognize the problem is character set. https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::p11_question_id:5783936214008 there is three way to config a utf8 database AL32UTF8, AL16UTF16 and also Unicode as describes by tom – mehran monster Jul 19 '18 at 05:22
  • Dear Boss what about only one insert without union? Can you test it with INSERT INTO CHARACTER_SET_MISMATCH_TEST (ID, VALUE) VALUES (?, ?) with null in the nvarchar2 column? – mehran monster Jul 19 '18 at 05:27
  • I thinks its about your connection pool character set and you must use ?characterEncoding=utf8 with mysql and you must do somthing like this with oracle – mehran monster Jul 19 '18 at 05:45