Erreur MySQL "valeur de chaîne incorrecte" lors de l'enregistrement de la chaîne Unicode dans Django

158

J'ai reçu un message d'erreur étrange lorsque j'ai essayé de sauvegarder first_name, last_name dans le modèle auth_user de Django.

Exemples d'échec

user = User.object.create_user(username, email, password)
user.first_name = u'Rytis'
user.last_name = u'Slatkevičius'
user.save()
>>> Incorrect string value: '\xC4\x8Dius' for column 'last_name' at row 104

user.first_name = u'Валерий'
user.last_name = u'Богданов'
user.save()
>>> Incorrect string value: '\xD0\x92\xD0\xB0\xD0\xBB...' for column 'first_name' at row 104

user.first_name = u'Krzysztof'
user.last_name = u'Szukiełojć'
user.save()
>>> Incorrect string value: '\xC5\x82oj\xC4\x87' for column 'last_name' at row 104

Réussir des exemples

user.first_name = u'Marcin'
user.last_name = u'Król'
user.save()
>>> SUCCEED

Paramètres MySQL

mysql> show variables like 'char%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | utf8                       | 
| character_set_connection | utf8                       | 
| character_set_database   | utf8                       | 
| character_set_filesystem | binary                     | 
| character_set_results    | utf8                       | 
| character_set_server     | utf8                       | 
| character_set_system     | utf8                       | 
| character_sets_dir       | /usr/share/mysql/charsets/ | 
+--------------------------+----------------------------+
8 rows in set (0.00 sec)

Jeu de caractères et classement de table

La table auth_user a le jeu de caractères utf-8 avec le classement utf8_general_ci.

Résultats de la commande UPDATE

Il n'a généré aucune erreur lors de la mise à jour des valeurs ci-dessus dans la table auth_user à l'aide de la commande UPDATE.

mysql> update auth_user set last_name='Slatkevičiusa' where id=1;
Query OK, 1 row affected, 1 warning (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select last_name from auth_user where id=100;
+---------------+
| last_name     |
+---------------+
| Slatkevi?iusa | 
+---------------+
1 row in set (0.00 sec)

PostgreSQL

Les valeurs ayant échoué répertoriées ci-dessus peuvent être mises à jour dans la table PostgreSQL lorsque j'ai basculé le backend de la base de données dans Django. C'est étrange.

mysql> SHOW CHARACTER SET;
+----------+-----------------------------+---------------------+--------+
| Charset  | Description                 | Default collation   | Maxlen |
+----------+-----------------------------+---------------------+--------+
...
| utf8     | UTF-8 Unicode               | utf8_general_ci     |      3 | 
...

Mais à partir de http://www.postgresql.org/docs/8.1/interactive/multibyte.html , j'ai trouvé ce qui suit:

Name Bytes/Char
UTF8 1-4

Est-ce que cela signifie que le caractère unicode a maxlen de 4 octets dans PostgreSQL mais 3 octets dans MySQL qui a causé l'erreur ci-dessus?

jack
la source
2
C'est un problème MySQL, pas Django: stackoverflow.com/questions/1168036/…
Vanuan

Réponses:

140

Aucune de ces réponses n'a résolu le problème pour moi. La cause première étant:

Vous ne pouvez pas stocker des caractères de 4 octets dans MySQL avec le jeu de caractères utf-8.

MySQL a une limite de 3 octets sur les caractères utf-8 (oui, c'est wack, bien résumé par un développeur Django ici )

Pour résoudre ce problème, vous devez:

  1. Modifiez votre base de données, table et colonnes MySQL pour utiliser le jeu de caractères utf8mb4 (disponible uniquement à partir de MySQL 5.5)
  2. Spécifiez le jeu de caractères dans votre fichier de paramètres Django comme ci-dessous:

settings.py

DATABASES = {
    'default': {
        'ENGINE':'django.db.backends.mysql',
        ...
        'OPTIONS': {'charset': 'utf8mb4'},
    }
}

Remarque: lors de la recréation de votre base de données, vous pouvez rencontrer le message `` La clé spécifiée était trop longue problème «La ».

La cause la plus probable est un CharField qui a une longueur_max de 255 et une sorte d'index dessus (par exemple, unique). Comme utf8mb4 utilise 33% d'espace en plus que utf-8, vous devrez réduire ces champs de 33%.

Dans ce cas, modifiez max_length de 255 à 191.

Alternativement, vous pouvez modifier votre configuration MySQL pour supprimer cette restriction, mais pas sans un piratage django

MISE À JOUR: Je viens de rencontrer à nouveau ce problème et j'ai fini par passer à PostgreSQL car je ne pouvais pas réduire mon VARCHARà 191 caractères.

Donturner
la source
13
cette réponse a besoin de beaucoup plus de votes positifs. Merci! Le vrai problème est que votre application peut fonctionner correctement pendant des années jusqu'à ce que quelqu'un essaie de saisir un caractère de 4 octets.
Michael Bylstra
2
C'est absolument la bonne réponse. Le paramètre OPTIONS est essentiel pour que django décode les caractères emoji et les stocke dans MySQL. Changer simplement le jeu de caractères mysql en utf8mb4 via des commandes SQL ne suffit pas!
Xerion
Il n'est pas nécessaire de mettre à jour le jeu de caractères de la table entière vers utf8mb4. Mettez simplement à jour le jeu de caractères des colonnes nécessaires. L' 'charset': 'utf8mb4'option dans les paramètres Django est également essentielle, comme l'a dit @Xerion. Enfin, le problème d'index est un gâchis. Supprimez l'index de la colonne, ou ne faites pas plus que 191, ou utilisez un à la TextFieldplace!
Rockallite
2
J'adore votre lien vers cette citation: Ceci est juste un autre cas où MySQL est délibérément et irréversiblement endommagé au cerveau. :)
Qback
120

J'ai eu le même problème et je l'ai résolu en modifiant le jeu de caractères de la colonne. Même si votre base de données a un jeu de caractères par défaut, utf-8je pense qu'il est possible que les colonnes de base de données aient un jeu de caractères différent dans MySQL. Voici la requête SQL que j'ai utilisée:

    ALTER TABLE database.table MODIFY COLUMN col VARCHAR(255)
    CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;
Gerdemb
la source
14
Pouah, j'ai changé tous les jeux de caractères sur tout ce que je pouvais jusqu'à ce que je relise vraiment cette réponse: les colonnes peuvent avoir leurs propres jeux de caractères, indépendamment des tables et de la base de données. C'est fou et c'était exactement mon problème.
markpasc
1
Cela a fonctionné pour moi aussi, en utilisant mysql avec les valeurs par défaut, dans un modèle TextField.
madprops
Cela a résolu mon problème. Le seul changement que j'ai fait a été d'utiliser utf8mb4 et utf8mb4_general_ci au lieu de utf8 / utf8_general_ci.
Michal Przysucha
70

Si vous rencontrez ce problème, voici un script python pour changer automatiquement toutes les colonnes de votre base de données mysql.

#! /usr/bin/env python
import MySQLdb

host = "localhost"
passwd = "passwd"
user = "youruser"
dbname = "yourdbname"

db = MySQLdb.connect(host=host, user=user, passwd=passwd, db=dbname)
cursor = db.cursor()

cursor.execute("ALTER DATABASE `%s` CHARACTER SET 'utf8' COLLATE 'utf8_unicode_ci'" % dbname)

sql = "SELECT DISTINCT(table_name) FROM information_schema.columns WHERE table_schema = '%s'" % dbname
cursor.execute(sql)

results = cursor.fetchall()
for row in results:
  sql = "ALTER TABLE `%s` convert to character set DEFAULT COLLATE DEFAULT" % (row[0])
  cursor.execute(sql)
db.close()
madprops
la source
4
Cette solution a résolu tous mes problèmes avec une application django qui stockait les chemins de fichiers et de répertoires. Ajoutez dbname comme base de données django et laissez-le fonctionner. A travaillé comme un charme!
Chris
1
Ce code ne fonctionnait pas pour moi jusqu'à ce que je l'ajoute db.commit()avant db.close().
Mark Erdmann
1
Cette solution évite-t-elle le problème abordé dans le commentaire @markpasc: `` ... caractères UTF-8 de 4 octets tels que les emoji dans le jeu de caractères utf8 de 3 octets de MySQL 5.1 ''
CatShoes
la solution m'aide quand je supprimais un enregistrement via django admin, je n'ai eu aucun problème lors de la création ou de l'édition ... bizarre! J'ai même pu supprimer directement dans la base de données
Javier Vieira
Dois-je le faire à chaque fois que je change de modèle?
Vanuan
25

S'il s'agit d'un nouveau projet, je supprimerais simplement la base de données et en créerais une nouvelle avec un jeu de caractères approprié:

CREATE DATABASE <dbname> CHARACTER SET utf8;
Vanuan
la source
Bonjour, merci de bien vouloir vérifier cette question stackoverflow.com/questions/46348817/…
King
Dans mon cas, notre base de données est créée par docker, donc pour corriger, j'ai ajouté ce qui suit à la commande db: command: instruction dans mon fichier de composition:- --character-set-server=utf8
followben
1
Aussi simple que cela. Merci @Vanuan
Enku
s'il ne s'agit pas d'un nouveau projet, nous obtenons une sauvegarde de db, la supprimons et la recréons avec le jeu de caractères utf8, puis restaurons la sauvegarde. Je l'ai fait dans mon projet qui n'était pas nouveau ...
Mohammad Reza
8

Je viens de trouver une méthode pour éviter les erreurs ci-dessus.

Enregistrer dans la base de données

user.first_name = u'Rytis'.encode('unicode_escape')
user.last_name = u'Slatkevičius'.encode('unicode_escape')
user.save()
>>> SUCCEED

print user.last_name
>>> Slatkevi\u010dius
print user.last_name.decode('unicode_escape')
>>> Slatkevičius

Est-ce la seule méthode pour enregistrer des chaînes comme celle-ci dans une table MySQL et la décoder avant de la rendre dans des modèles pour l'affichage?

jack
la source
12
J'ai un problème similaire, mais je ne suis pas d'accord pour dire que c'est une solution valable. Lorsque vous .encode('unicode_escape')ne stockez pas réellement de caractères Unicode dans la base de données. Vous obligez tous les clients à désencoder avant de les utiliser, ce qui signifie que cela ne fonctionnera pas correctement avec django.admin ou toutes sortes d'autres choses.
muudscope
3
Bien qu'il semble désagréable de stocker des codes d'échappement au lieu de caractères, c'est probablement l'un des rares moyens de sauvegarder des caractères UTF-8 de 4 octets tels que les emoji dans le jeu de caractères de 3 octets de MySQL 5.1 utf8.
markpasc
2
Il existe un encodage appelé utf8mb4qui permet de stocker plus que le plan multilingue de base. Je sais, vous penseriez que "UTF8" est tout ce dont vous avez besoin pour stocker entièrement Unicode. Eh bien, whaddaya sais, ce n'est pas le cas. Voir dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
Mihai Danila
@jack, vous voudrez peut-être envisager de changer la réponse acceptée par une réponse plus utile
donturner
c'est une solution de contournement possible, mais je ne recommande pas de l'utiliser aussi (comme le préconise @muudscope). Je ne peux toujours pas stocker, par exemple, des emoji dans des bases de données mysql. Quelqu'un l'a-t-il accompli?
Marcelo Sardelich
6

Vous pouvez changer le classement de votre champ de texte en UTF8_general_ci et le problème sera résolu.

Remarquez que cela ne peut pas être fait dans Django.

Wei An
la source
1

Vous n'essayez pas de sauvegarder des chaînes Unicode, vous essayez de sauvegarder des chaînes d'octets dans l'encodage UTF-8. Faites-en de véritables littéraux de chaîne Unicode:

user.last_name = u'Slatkevičius'

ou (lorsque vous n'avez pas de littéraux de chaîne) décodez-les en utilisant l'encodage utf-8:

user.last_name = lastname.decode('utf-8')
Thomas Wouters
la source
@Thomas, j'ai essayé exactement ce que vous avez dit mais cela soulève toujours les mêmes erreurs.
jack
0

Modifiez simplement votre table, pas besoin de quoi que ce soit. exécutez simplement cette requête sur la base de données. ALTER TABLE table_nameCONVERT TO CARACTER SET utf8

cela fonctionnera certainement.

Rishabh Jhalani
la source
0

Amélioration de la réponse @madprops - solution en tant que commande de gestion django:

import MySQLdb
from django.conf import settings

from django.core.management.base import BaseCommand


class Command(BaseCommand):

    def handle(self, *args, **options):
        host = settings.DATABASES['default']['HOST']
        password = settings.DATABASES['default']['PASSWORD']
        user = settings.DATABASES['default']['USER']
        dbname = settings.DATABASES['default']['NAME']

        db = MySQLdb.connect(host=host, user=user, passwd=password, db=dbname)
        cursor = db.cursor()

        cursor.execute("ALTER DATABASE `%s` CHARACTER SET 'utf8' COLLATE 'utf8_unicode_ci'" % dbname)

        sql = "SELECT DISTINCT(table_name) FROM information_schema.columns WHERE table_schema = '%s'" % dbname
        cursor.execute(sql)

        results = cursor.fetchall()
        for row in results:
            print(f'Changing table "{row[0]}"...')
            sql = "ALTER TABLE `%s` convert to character set DEFAULT COLLATE DEFAULT" % (row[0])
            cursor.execute(sql)
        db.close()

J'espère que cela aide tout le monde sauf moi :)

Ron
la source