Lors de l'utilisation de JDBC, les paramètres NLS sont affectés par les paramètres régionaux Java.

Environnement d'exécution

Les principales versions de logiciel utilisées pour rédiger cet article sont les suivantes. Docker et Oracle Official Docker Image sont utilisés pour créer la base de données Oracle.

software version, edition
Oracle Database 12c Oracle Database 12c Standard Edition Release 12.2.0.1.0 - 64bit Production
ojdbc8.jar (12c JDBC Driver) Oracle 12.2.0.1.0 JDBC 4.2 compiled with javac 1.8.0_91 on Tue_Dec_13_06:08:31_PST_2016
javac javac 11.0.4
java openjdk version "11.0.4" 2019-07-16

Aperçu

Comme il existe différentes méthodes de configuration pour les paramètres NLS d'Oracle Database, "Je ne sais pas quelle valeur de paramètre affecte comment!" Se produit, mais en utilisant le pilote JDBC pour se connecter à Oracle Database Si tel est le cas, les paramètres NLS peuvent changer en fonction des paramètres régionaux Java du client.

[Guide d'installation de la base de données pour la langue de connexion client Linux-Setting et les préférences Localt](https://docs.oracle.com/cd/E96517_01/ladbi/setting-language-preferences-for-client- Extrait de connections.html # GUID-78A71337-2199-4FED-B0F1-D313F769B22C):

Les applications Java qui utilisent Oracle JDBC pour se connecter à la base de données Oracle n'utilisent pas NLS_LANG. Au lieu de cela, Oracle JDBC mappe les paramètres régionaux par défaut de la machine virtuelle Java exécutant votre application aux paramètres de langue et de territoire de la base de données Oracle.

Expérience

Expérimentons. Voici une application Java qui utilise JDBC et génère tout le contenu de V $ NLS_PARAMETERS qui stocke les paramètres actuels des paramètres NLS.

Main1.java


import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class Main1 {
    public static void main(String[] args) {
        String url = "jdbc:oracle:thin:@//192.168.99.100:1521/ORCLPDB1";
        String user = "dev1";
        String password = "password";

        try (Connection c = DriverManager.getConnection(url, user, password)) {
            Statement stmt = c.createStatement();
            ResultSet rs = stmt.executeQuery("SELECT * FROM V$NLS_PARAMETERS ORDER BY PARAMETER");
            while (rs.next()) {
                String parameter = rs.getString("PARAMETER");
                String value = rs.getString("VALUE");
                System.out.printf("%s = %s%n", parameter, value);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Faisons cela en modifiant les paramètres régionaux Java. Tout d'abord, le japonais.

$ java -Duser.language=ja -Duser.country=JP -cp .:../lib/ojdbc8.jar Main1
NLS_CALENDAR = GREGORIAN
NLS_CHARACTERSET = AL32UTF8
NLS_COMP = BINARY
NLS_CURRENCY = ¥
NLS_DATE_FORMAT = RR-MM-DD
NLS_DATE_LANGUAGE = JAPANESE
NLS_DUAL_CURRENCY = \
NLS_ISO_CURRENCY = JAPAN
NLS_LANGUAGE = JAPANESE
NLS_LENGTH_SEMANTICS = BYTE
NLS_NCHAR_CHARACTERSET = AL16UTF16
NLS_NCHAR_CONV_EXCP = FALSE
NLS_NUMERIC_CHARACTERS = .,
NLS_SORT = BINARY
NLS_TERRITORY = JAPAN
NLS_TIMESTAMP_FORMAT = RR-MM-DD HH24:MI:SSXFF
NLS_TIMESTAMP_TZ_FORMAT = RR-MM-DD HH24:MI:SSXFF TZR
NLS_TIME_FORMAT = HH24:MI:SSXFF
NLS_TIME_TZ_FORMAT = HH24:MI:SSXFF TZR

Vient ensuite l'anglais.

$ java -Duser.language=en -Duser.country=US -cp .:../lib/ojdbc8.jar Main1
NLS_CALENDAR = GREGORIAN
NLS_CHARACTERSET = AL32UTF8
NLS_COMP = BINARY
NLS_CURRENCY = $
NLS_DATE_FORMAT = DD-MON-RR
NLS_DATE_LANGUAGE = AMERICAN
NLS_DUAL_CURRENCY = $
NLS_ISO_CURRENCY = AMERICA
NLS_LANGUAGE = AMERICAN
NLS_LENGTH_SEMANTICS = BYTE
NLS_NCHAR_CHARACTERSET = AL16UTF16
NLS_NCHAR_CONV_EXCP = FALSE
NLS_NUMERIC_CHARACTERS = .,
NLS_SORT = BINARY
NLS_TERRITORY = AMERICA
NLS_TIMESTAMP_FORMAT = DD-MON-RR HH.MI.SSXFF AM
NLS_TIMESTAMP_TZ_FORMAT = DD-MON-RR HH.MI.SSXFF AM TZR
NLS_TIME_FORMAT = HH.MI.SSXFF AM
NLS_TIME_TZ_FORMAT = HH.MI.SSXFF AM TZR

J'ai trouvé que les paramètres NLS changent en fonction des paramètres régionaux côté client, même si je n'ai modifié aucun paramètre du côté de la base de données Oracle.

Ce qui est effrayant

Ce qui est effrayant avec ce comportement, c'est que les choses suivantes peuvent se produire:

Le fait est que les applications Java peuvent se comporter différemment selon l'environnement. Si vous êtes un programmeur Java expérimenté, vous devriez essayer une programmation qui ne dépend pas du SGBD ou des paramètres régionaux, mais il y a une réalité qui ne l'est pas (´ ・ ω ・ `)

Exemple spécifique: conversion de type implicite du type chaîne au type TIMESTAMP

Nous concluons par un exemple plus concret de la façon dont ce comportement cause des problèmes.

Supposons que vous ayez une table appelée ʻuserscomme ceci: Supposons que cette table ait un nom d'utilisateurnom et une heure de mise à jour record ʻupdated_at (une configuration courante).

CREATE TABLE users (
  name VARCHAR2(256 CHAR),
  updated_at TIMESTAMP
)

Le Main2.java suivant est une application Java qui insère des données dans cette table ʻusers`.

Main2.java


import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class Main2 {
    public static void main(String[] args) {
        String url = "jdbc:oracle:thin:@//192.168.99.100:1521/ORCLPDB1";
        String user = "dev1";
        String password = "password";

        try (Connection c = DriverManager.getConnection(url, user, password)) {
            PreparedStatement pstmt = c.prepareStatement("INSERT INTO users (name, updated_at) VALUES (?, ?)");
            pstmt.setString(1, "nekoTheShadow");
            pstmt.setString(2, "20200117");
            int count = pstmt.executeUpdate();

            System.out.printf("PreparedStatement::executeUpdate = %d%n", count);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

__ Ce à quoi vous devez faire attention ici, c'est que vous essayez d'insérer une valeur de type chaîne (" 20200117 ") dans la colonne de type TIMESTAMP ʻupdated_at. __ Dans ce cas, la fonction "conversion de type implicite" d'Oracle Database renvoie le type de chaîne au type TIMESTAMP`. Et la manière dont la "conversion de type implicite" est effectuée dépend fortement des paramètres NLS.

Maintenant, exécutons ce Main2.java dans une langue différente. Tout d'abord, le japonais.

$ java -Duser.language=ja -Duser.country=JP -cp .:../lib/ojdbc8.jar Main2
PreparedStatement::executeUpdate = 1

Il semble que les données ont été insérées correctement. Ensuite, exécutez-le en anglais.

$ java -Duser.language=en -Duser.country=US -cp .:../lib/ojdbc8.jar Main2
java.sql.SQLDataException: ORA-01843: not a valid month

        at oracle.jdbc.driver.T4CTTIoer11.processError(T4CTTIoer11.java:494)
        at oracle.jdbc.driver.T4CTTIoer11.processError(T4CTTIoer11.java:446)
        at oracle.jdbc.driver.T4C8Oall.processError(T4C8Oall.java:1054)
        at oracle.jdbc.driver.T4CTTIfun.receive(T4CTTIfun.java:623)
        at oracle.jdbc.driver.T4CTTIfun.doRPC(T4CTTIfun.java:252)
        at oracle.jdbc.driver.T4C8Oall.doOALL(T4C8Oall.java:612)
        at oracle.jdbc.driver.T4CPreparedStatement.doOall8(T4CPreparedStatement.java:226)
        at oracle.jdbc.driver.T4CPreparedStatement.doOall8(T4CPreparedStatement.java:59)
        at oracle.jdbc.driver.T4CPreparedStatement.executeForRows(T4CPreparedStatement.java:910)
        at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout(OracleStatement.java:1119)
        at oracle.jdbc.driver.OraclePreparedStatement.executeInternal(OraclePreparedStatement.java:3780)
        at oracle.jdbc.driver.T4CPreparedStatement.executeInternal(T4CPreparedStatement.java:1343)
        at oracle.jdbc.driver.OraclePreparedStatement.executeLargeUpdate(OraclePreparedStatement.java:3865)
        at oracle.jdbc.driver.OraclePreparedStatement.executeUpdate(OraclePreparedStatement.java:3845)
        at oracle.jdbc.driver.OraclePreparedStatementWrapper.executeUpdate(OraclePreparedStatementWrapper.java:1061)
        at Main2.main(Main2.java:16)
Caused by: Error : 1843, Position : 51, Sql = INSERT INTO users (name, updated_at) VALUES (:1 , :2 ), OriginalSql = INSERT INTO users (name, updated_at) VALUES (?, ?), Error Msg = ORA-01843: not a valid month

        at oracle.jdbc.driver.T4CTTIoer11.processError(T4CTTIoer11.java:498)
        ... 15 more

Dans le cas de locale = Japan, cela a fonctionné comme prévu, mais dès que je suis passé à locale = English, j'ai obtenu une exception. De plus, il est difficile de dire en un coup d'œil que le contenu de l'exception est plutôt déroutant, ou du moins un problème lié aux paramètres régionaux. La solution n'est pas de faire une conversion de type implicite, ou d'utiliser le type «Date» de Java.

Recommended Posts

Lors de l'utilisation de JDBC, les paramètres NLS sont affectés par les paramètres régionaux Java.
Différences de code lors de l'utilisation du système de longueur en Java
Lorsque vous utilisez le constructeur de la classe Date de Java, la date avance de 1900.
Changer la version de java installée par SDKMAN lors du déplacement de répertoires
Voyons le résultat de l'exécution lors de l'utilisation de l'instruction préparée dans JDBC
Comment résoudre l'erreur inconnue apparue lors de l'utilisation de slf4j en Java
La version d'Elasticsearch que vous utilisez est-elle compatible avec Java 11?
Comparaison Java à l'aide de la méthode compareTo ()
Exception inattendue lors de l'utilisation de DateTimeFormatter de Java