Retour d’expérience : stocker des chaînes UTF-8 dans une base de données ISO8859-1

Problématique

Les utilisateurs d'une application web doivent pouvoir saisir un texte libre (de taille inférieure à 255 caractères). Ces textes sont ensuite rendus persistants en base de données.
Les utilisateurs doivent pouvoir saisir le caractère ou des caractères non ISO8859-1 comme "œ" et les rechercher en base de données.

La base de données est configurée en ISO8859-1 et la persistance des caractères comme déclenche des erreurs.

Remarque

Sur un clavier AZERTY récent, le caractère euro peut s'obtenir directement au clavier.
Quant aux autres caractères non ISO8859-1, ils sont automatiquement insérés dans word par le correcteur orthographique. Autrement dit, un utilisateur qui saisit son commentaire sous Word et fait un copier/coller dans le TextArea de l'application web insére automatiquement ce type de caractère.

Noter que le œ est bien un caractère spécifique ; on ne peut remplacer toute séquence oe par "œ". L'e dans l'o est en effet une ligature linguistique et pas esthétique (elle a un impact sur la prononciation). Les mots coefficients, coexistance, moelle et foehn par exemple ne peuvent pas être écrits avec le caractère "œ".

Contexte technique

Une application web
Une base de données Sybase configurée en ISO8859-1 (et dont la configuration est non modifiable)
Le framework javascript DOJO
TOMCAT
JRE 1.6


Rappels : caractères supportés par les normes

Les normes ISO codent leur caractères sur 8 bits (et les 127 premières valeurs sont celles de l'ASCII).

La norme ISO8859-1 qui recouvre les caractères utilisés par une bonne partie des pays européens couvre pratiquement l'ensemble des caractères français sauf les caractères œ et Œ et Ÿ (et pas le caractère euro).

La norme ISO8859-15 intègre les caractères œ et Œ et Ÿ ainsi que l'euro "€", elle couvre l'ensemble des caractères utilisés en Français. Malheureusement, cela ne résout pas le problème des caractères UNICODE non ISO. De plus, l'intégration de ces nouveaux caractères dans ISO8859-15 s'est fait en supprimant d'autres caractères comme "½" et "¼" (présents dans ISO8859-1) qui sont toujours générés automatiquement par word.
Autrement dit, même en ISO8859-15, un copier/coller peut insérer des caractères non couverts.

UNICODE est une norme destinée à représenter tous les caractères. UNICODE utiliser un stockage sur 32 bits (de U+0000 à U+FFFF).

UTF-8 est une des normes permettant les stockage des caractères UNICODE : UTF-8 utilise un nombre d'octets variables pour stocker chaque caractère d'une chaîne.

Solution 1 : utiliser UTF8 au niveau de l'application

Il faut rajouter un charset=UTF8 sur tous les appels AJAX (par exemple dans DOJO).
Il faut rajouter un request.setCharacterEncoding("UTF-8") dans chaque servlet de l'application (voire dans un filtre générique sur les .do struts, les jsp, les servlets).

Même si le charset par défaut des servlets engine est ISO8859-1, il n'est pas nécessaire de paramétrer TOMCAT pour qu'il utilise de façon générale UTF-8 (si on souhaite néanmoins le faire, il faut modifier la balise CONNECTOR dans le fichier de configuration pour lui positionner le charset).

Rappels : stockage des caractères selon les normes

UTF8 code les caractères sur un nombre d'octets variables.
Les caractères ASCII (7 bits) sont stockés sur un seul octet (pour les caractères ASCII, il n'y a pas de différence entre un codage ISO et un codage UTF-8).
Les caractères ISO non ascii (les codes entre 128 et 255) sont stockés sur 2 octets en UTF8.
Les caractères de code supérieurs à 255 peuvent réclamer jusqu'à 4 octets pour leur codage (dans l'ancienne version UNICODE, ils pouvaient réclamer jusqu'à 6 octets).

Si l'on précise le charset UTF-8 dans l'url du driver JDBC Sybase utilisé, la base stocke des caractères UTF-8.
Attention ! la base continue à être configurée en ISO8859-1, mais on l'attaque avec un driver JDBC UTF-8.

Ainsi, si on insère la chaîne "sœur" la colonne contient 5 caractères :
"s", "
Å","<control> ","u","r" les deux caractères insérés à la place de œ stocke ont pour code UNICODE hexadécimal U+00C5 et U+OO93 (ce dernier étant un caractère de contrôle non imprimable).
Autrement dit, la base stocke bien les caractères de l'UTF-8. Si on fait un SELECT on récupère bien la chaîne correcte en JAVA.
Cependant, si on teste la taille de la colonne en base de données (via datalength ou char_length sous Sybase), on obtient 5 caractères (et pas 4).
Cela a deux conséquences :
si d'autres applications accèdent aux mêmes données, il est nécessaire de les modifier pour qu'elles gèrent l'UTF-8,
la taille est différente en nombre de caractères (en java ou en javascript) et en bases de données.
C'est à dire que si on accepte des saisies de 255 caractères, on doit utiliser une déclaration plus longue en base de données (et pas un VARCHAR 255).

[remarque : Java gère nativement UTF-8
java manipule directement un codage interne des caractères en UTF-8, autrement dit, pas de différence sur les String Java. la seule conséquence est la méthode getBytes qui rend un nombre d'octets différents selon l'encoding.
]

Bilan :

Le problème principal est qu'en utilisant UTF-8, le stockage de tous les caractères ISO (non ascii) est modifié également : et en français, on les utilise beaucoup !
En effet, les codes compris entre U+0080 (128) et U+00FF (255), mêmes s'ils correspondent aux caractères ISO8859-1, sont codés sur 2 octets (entre C2 80 et C3 BF).
Du coup, toutes les chaînes comportant des caractères ISO français (hors ascii), comme les accents ont une longueur supérieure au nombre de caractères.

Une recherche SQL via un Select LIKE ne fonctionnant plus pour tous les caractères codés sur 2 octets, la requête doit elle aussi être transformée : tout caractère non ascii doit être transformé en son équivalent UTF-8. Ainsi une recherche sur la chaîne "sœur" doit être d'abord traduite en UTF-8. [et il peut y avoir des problèmes au niveau du moteur de recherche de la base de données]

Solution 2 : escaper les caractères supérieurs à 255 et conserver une base en ISO8859-1

L'idée est de communiquer avec la base via ISO8859-1 mais d'escaper tous les caractères de code supérieurs à 255.
Du coup, la plupart des caractères français (sauf les oe , ÿ et le caractère €) sont stockés tels quels dans la base.
Seule la présence de caractères non ISO8859-1 dans une chaîne modifie la longueur de cette chaîne en base de données.

On peut choisir plusieurs escape possibles :
à la URL encoded (avec le caractère %).
Ce qui donnerait pour la chaîne "sœur" la chaîne "s%C5%93ur" Noter qu'il ne s'agit pas d'une véritable URL encoded parce que celle-ci escape tous les caractères non ascii.

avec une syntaxe à la java "suC593ur"

avec des entités nommées ou numériques HTML : "s&oelig;ur"

Conséquence :

Il faut en parallèle augmenter la taille de la colonne "commentaire", pour qu'elle fasse plus de 255 caractères (même si les contrôles javascript et java limitent les saisies à 255 caractères effectifs).
Les autres application utilisant la base doivent potentiellement être adaptées si elles utilisent le champ commentaire (par exemple par transliteration cf. solution 3 décrite plus bas).

Conclusion :

La seule vraie solution étant la configuration de la base de données en UTF-8, on n'a ensuite que des solutions qui sont des pis-aller.
La solution 2, même si elle est hybride, a l'avantage de diminuer le surcoût en terme de stockage dans la base de données mais elle nécessite de gérer par programme l'espace/unescape des chaînes traitées.

La solution 1, marche automatiquement, reste à savoir si la démarche consistant à attaquer une base configurée en ISO8859-1 avec un driver UTF-8 est généralisable à toutes les bases de données.

Dans tous les cas de figure, les requêtes de sélection LIKE (ou REGEXP mySQL) doivent être modifiées pour tenir compte de l'escape choisi

Solution 3 : escape "translitération"

Citons également une troisième solution mais qui contrairement aux autres, déclenche une perte d'information. Il s'agit de faire des translitérations des caractères fautifs.
Par exemple, de remplacer les occurences de ÿ par y, de œ par oe...
De la même façon, on peut translitérer le caractère en une chaîne "euro".

Cependant, ça ne résout pas le problème pour les caractères UTF8. ça peut cependant être un traitement utilisé pour les autres applications partageant les mêmes données en base.

Remarque :
la recherche via LIKE doit être adaptée : on peut par exemple accepter les recherches sur les orthographes soeur et sœur, utiliser un soundex ou une expression régulière appropriée.

Les commentaires ont été fermés.