Error de "valor de cadena incorrecto" de MySQL al guardar una cadena Unicode en Django

158

Recibí un extraño mensaje de error cuando intenté guardar first_name, last_name en el modelo auth_user de Django.

Ejemplos fallidos

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

Ejemplos de éxito

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

Configuración de 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)

Conjunto de caracteres de tabla y colación

La tabla auth_user tiene un conjunto de caracteres utf-8 con la clasificación utf8_general_ci.

Resultados del comando ACTUALIZAR

No generó ningún error al actualizar los valores anteriores a la tabla auth_user mediante el comando 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

Los valores fallidos enumerados anteriormente se pueden actualizar en la tabla PostgreSQL cuando cambié el backend de la base de datos en Django. Es extraño.

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

Pero en http://www.postgresql.org/docs/8.1/interactive/multibyte.html , encontré lo siguiente:

Name Bytes/Char
UTF8 1-4

¿Significa que unicode char tiene un máximo de 4 bytes en PostgreSQL pero 3 bytes en MySQL que causó el error anterior?

Jack
fuente
2
Es un problema de MySQL, no Django: stackoverflow.com/questions/1168036/…
Vanuan

Respuestas:

140

Ninguna de estas respuestas resolvió el problema para mí. La causa raíz es:

No puede almacenar caracteres de 4 bytes en MySQL con el conjunto de caracteres utf-8.

MySQL tiene un límite de 3 bytes en los caracteres utf-8 (sí, es wack, resumido muy bien por un desarrollador de Django aquí )

Para resolver esto necesitas:

  1. Cambie su base de datos, tabla y columnas de MySQL para usar el conjunto de caracteres utf8mb4 (solo disponible desde MySQL 5.5 en adelante)
  2. Especifique el conjunto de caracteres en su archivo de configuración de Django de la siguiente manera:

settings.py

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

Nota: Al recrear su base de datos, puede encontrarse con el problema 'La clave especificada fue demasiado larga '.

La causa más probable es una CharFieldque tiene una longitud máxima de 255 y algún tipo de índice (por ejemplo, único). Como utf8mb4 usa un 33% más de espacio que utf-8, necesitará hacer que estos campos sean un 33% más pequeños.

En este caso, cambie la longitud máxima de 255 a 191.

Alternativamente, puede editar su configuración de MySQL para eliminar esta restricción, pero no sin algún hacker de django

ACTUALIZACIÓN: Acabo de encontrarme con este problema nuevamente y terminé cambiando a PostgreSQL porque no pude reducir mi VARCHARa 191 caracteres.

donturner
fuente
13
Esta respuesta necesita mucho, mucho más votos a favor. ¡Gracias! El verdadero problema es que su aplicación puede funcionar bien durante años hasta que alguien intente ingresar un carácter de 4 bytes.
Michael Bylstra
2
Esta es absolutamente la respuesta correcta. La configuración de OPCIONES es crítica para que django decodifique caracteres emoji y los almacene en MySQL. ¡Simplemente cambiar mysql charset a utf8mb4 a través de comandos SQL no es suficiente!
Xerion
No es necesario actualizar el conjunto de caracteres de toda la tabla a utf8mb4. Simplemente actualice el conjunto de caracteres de las columnas necesarias. También la 'charset': 'utf8mb4'opción en la configuración de Django es crítica, como dijo @Xerion. Finalmente, el problema del índice es un desastre. ¡Elimine el índice en la columna, o haga que su longitud no sea superior a 191, o use un TextFielden su lugar!
Rockallite
2
Me encanta su enlace a esta cita: este es solo otro caso de que MySQL sufre un daño cerebral intencional e irreversible. :)
Qback
120

Tuve el mismo problema y lo resolví cambiando el conjunto de caracteres de la columna. Aunque su base de datos tiene un conjunto de caracteres predeterminado utf-8, creo que es posible que las columnas de la base de datos tengan un conjunto de caracteres diferente en MySQL. Aquí está la consulta SQL que utilicé:

    ALTER TABLE database.table MODIFY COLUMN col VARCHAR(255)
    CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;
gerdemb
fuente
14
Ugh, cambié todos los conjuntos de caracteres en todo lo que pude hasta que realmente volví a leer esta respuesta: las columnas pueden tener sus propios conjuntos de caracteres, independientemente de las tablas y la base de datos. Eso es una locura y también fue exactamente mi problema.
markpasc
1
Esto también funcionó para mí, usando mysql con los valores predeterminados, en un modelo TextField.
madprops
Esto resolvió mi problema. El único cambio que hice fue usar utf8mb4 y utf8mb4_general_ci en lugar de utf8 / utf8_general_ci.
Michal Przysucha
70

Si tiene este problema, aquí hay un script de Python para cambiar todas las columnas de su base de datos mysql automáticamente.

#! /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
fuente
44
Esta solución resolvió todos mis problemas con una aplicación django que almacenaba rutas de archivos y directorios. Agregue dbname como su base de datos django y déjelo correr. ¡Trabajado como un encanto!
Chris
1
Este código no funcionó para mí hasta que agregué db.commit()antes db.close().
Mark Erdmann
1
¿Esta solución evita el problema discutido en el comentario de @markpasc: '... caracteres UTF-8 de 4 bytes como emoji en el conjunto de caracteres utf8 de 3 bytes de MySQL 5.1'
CatShoes
la solución me ayudó cuando estaba eliminando un registro a través de django admin, no tuve ningún problema al crear o editar ... ¡extraño! Incluso pude eliminar directamente en la base de datos
Javier Vieira
¿Debo hacer esto cada vez que cambie el modelo?
Vanuan
25

Si es un proyecto nuevo, simplemente dejaría caer la base de datos y crearía una nueva con un juego de caracteres adecuado:

CREATE DATABASE <dbname> CHARACTER SET utf8;
Vanuan
fuente
Hola, amablemente, ayuda a revisar esta pregunta stackoverflow.com/questions/46348817/…
King
En mi caso, nuestra base de datos es creada por Docker, así que para solucionarlo agregué lo siguiente a la instrucción db: command: en mi archivo de composición:- --character-set-server=utf8
followben
1
Tan sencillo como eso. Gracias @Vanuan
Enku
Si este no es un proyecto nuevo, obtenemos una copia de seguridad de db, la soltamos y la recreamos con utf8 charset y luego restauramos la copia de seguridad. Lo hice en mi proyecto que no era nuevo ...
Mohammad Reza
8

Acabo de descubrir un método para evitar los errores anteriores.

Guardar en la base de datos

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

¿Es este el único método para guardar cadenas como esa en una tabla MySQL y decodificarla antes de renderizar en plantillas para mostrar?

Jack
fuente
12
Tengo un problema similar, pero no estoy de acuerdo con que esta sea una solución válida. Cuando en .encode('unicode_escape')realidad no está almacenando caracteres Unicode en la base de datos. Estás obligando a todos los clientes a que se descodifiquen antes de usarlos, lo que significa que no funcionará correctamente con django.admin o todo tipo de otras cosas.
muudscope
3
Si bien parece desagradable almacenar códigos de escape en lugar de caracteres, esta es probablemente una de las pocas formas de guardar caracteres UTF-8 de 4 bytes como emoji en el utf8conjunto de caracteres de 3 bytes de MySQL 5.1 .
markpasc
2
Hay una codificación llamada utf8mb4que permite almacenar más que el plano multilingüe básico. Lo sé, pensarías que "UTF8" es todo lo que se necesita para almacenar Unicode por completo. Bueno, ya sabes, no lo es. Ver dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
Mihai Danila
@jack es posible que desee considerar cambiar la respuesta aceptada a una que sea más útil
donturner
es una solución viable, pero no recomiendo usarlo también (como lo recomienda @muudscope). Todavía no puedo almacenar, por ejemplo, emoji en bases de datos mysql. ¿Alguien lo ha logrado?
Marcelo Sardelich
6

Puede cambiar la clasificación de su campo de texto a UTF8_general_ci y el problema se resolverá.

Tenga en cuenta que esto no se puede hacer en Django.

Wei An
fuente
1

No está intentando guardar cadenas Unicode, está intentando guardar cadenas de bytes en la codificación UTF-8. Conviértelos en literales de cadena Unicode reales

user.last_name = u'Slatkevičius'

o (cuando no tiene literales de cadena) decodifíquelos usando la codificación utf-8:

user.last_name = lastname.decode('utf-8')
Thomas Wouters
fuente
@Thomas, intenté exactamente lo que dijiste pero aún genera los mismos errores.
Jack
0

Simplemente modifique su mesa, sin necesidad de nada. simplemente ejecute esta consulta en la base de datos. ALTERAR TABLA table_nameCONVERTIR AL CARACTER SET utf8

Definitivamente funcionará.

Rishabh Jhalani
fuente
0

Mejora a la respuesta @madprops: solución como un comando de gestión de 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()

Espero que esto ayude a cualquiera menos a mí :)

Ron
fuente