Errore "valore stringa errato" di MySQL quando si salva la stringa unicode in Django


158

Ho ricevuto uno strano messaggio di errore quando ho provato a salvare first_name, last_name nel modello auth_user di Django.

Esempi falliti

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

Successi esempi

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

Impostazioni di 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)

Tabella set di caratteri e regole di confronto

La tabella auth_user ha un set di caratteri utf-8 con regole di confronto utf8_general_ci.

Risultati del comando UPDATE

Non ha generato alcun errore durante l'aggiornamento dei valori precedenti alla tabella auth_user utilizzando il 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

I valori non elencati sopra possono essere aggiornati nella tabella PostgreSQL quando ho cambiato il back-end del database in Django. È strano.

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

Ma da http://www.postgresql.org/docs/8.1/interactive/multibyte.html , ho trovato quanto segue:

Name Bytes/Char
UTF8 1-4

Significa che il carattere Unicode ha un massimo di 4 byte in PostgreSQL ma 3 byte in MySQL che ha causato l'errore sopra?


2
E 'un problema di MySQL, non Django: stackoverflow.com/questions/1168036/...
Vanuan

Risposte:


140

Nessuna di queste risposte ha risolto il problema per me. La causa principale è:

Non è possibile memorizzare caratteri a 4 byte in MySQL con il set di caratteri utf-8.

MySQL ha un limite di 3 byte per i caratteri utf-8 (sì, è strano, ben riassunto da uno sviluppatore di Django qui )

Per risolvere questo è necessario:

  1. Modifica il database, la tabella e le colonne di MySQL per utilizzare il set di caratteri utf8mb4 (disponibile solo da MySQL 5.5 in poi)
  2. Specificare il set di caratteri nel file delle impostazioni di Django come di seguito:

settings.py

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

Nota: quando si ricrea il database è possibile che si verifichi il problema "La chiave specificata era troppo lunga ".

La causa più probabile è una CharFieldche ha una max_length di 255 e un qualche tipo di indice su di essa (es. Unico). Poiché utf8mb4 utilizza il 33% di spazio in più rispetto a utf-8, sarà necessario ridurre questi campi del 33%.

In questo caso, modifica la lunghezza massima da 255 a 191.

In alternativa puoi modificare la tua configurazione MySQL per rimuovere questa limitazione ma non senza un po 'di hackeraggio di django

AGGIORNAMENTO: Ho appena riscontrato di nuovo questo problema e ho finito per passare a PostgreSQL perché non ero in grado di ridurre i miei VARCHARa 191 caratteri.


13
questa risposta ha bisogno di molti più voti. Grazie! Il vero problema è che l'applicazione potrebbe funzionare correttamente per anni fino a quando qualcuno non tenta di inserire un carattere a 4 byte.
Michael Bylstra,

2
Questa è assolutamente la risposta giusta. L'impostazione OPTIONS è fondamentale per decodificare i caratteri emoji di django e memorizzarli in MySQL. Basta cambiare il set di caratteri mysql in utf8mb4 tramite comandi SQL non è sufficiente!
Xerion,

Non è necessario aggiornare il set di caratteri dell'intera tabella su utf8mb4. Basta aggiornare il set di caratteri delle colonne necessarie. Anche l' 'charset': 'utf8mb4'opzione nelle impostazioni di Django è fondamentale, come ha detto @Xerion. Infine, il problema dell'indice è un casino. Rimuovi l'indice sulla colonna o allunga non più di 191 o usa TextFieldinvece un !
Rockallite,

2
Adoro il tuo link a questa citazione: questo è solo un altro caso in cui MySQL è intenzionalmente e irreversibilmente danneggiato dal cervello. :)
Qback

120

Ho avuto lo stesso problema e l'ho risolto cambiando il set di caratteri della colonna. Anche se il tuo database ha un set di caratteri predefinito, utf-8penso che sia possibile per le colonne del database avere un set di caratteri diverso in MySQL. Ecco il QUERY SQL che ho usato:

    ALTER TABLE database.table MODIFY COLUMN col VARCHAR(255)
    CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;

14
Ugh, ho cambiato tutti i set di caratteri su tutto quello che potevo fino a quando non ho letto di nuovo questa risposta: le colonne possono avere i propri set di caratteri, indipendentemente dalle tabelle e dal database. È pazzesco ed è stato esattamente il mio problema.
markpasc,

1
Questo ha funzionato anche per me, usando mysql con le impostazioni predefinite, in un modello TextField.
madprops,

Questo ha risolto il mio problema. L'unica modifica che ho fatto è stata usare utf8mb4 e utf8mb4_general_ci invece di utf8 / utf8_general_ci.
Michal Przysucha,

70

Se hai questo problema, ecco uno script Python per cambiare automaticamente tutte le colonne del tuo database 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()

4
Questa soluzione ha risolto tutti i miei problemi con un'app django che memorizzava i percorsi di file e directory. Inserisci dbname come database django e lascialo funzionare. Ha funzionato come un fascino!
Chris,

1
Questo codice non ha funzionato per me fino a quando non l'ho aggiunto db.commit()prima db.close().
Mark Erdmann,

1
Questa soluzione evita il problema discusso nel commento di @markpasc: "... caratteri UTF-8 a 4 byte come emoji nel set di caratteri utf8 a 3 byte di MySQL 5.1"
CatShoes

la soluzione mi ha aiutato quando stavo cancellando un record tramite django admin, non ho avuto nessun problema durante la creazione o l'editing ... strano! Sono stato anche in grado di eliminare direttamente nel db
Javier Vieira il

Devo farlo ogni volta che cambio il modello?
Vanuan,

25

Se è un nuovo progetto, lascerei cadere il database e ne creerei uno nuovo con un set di caratteri appropriato:

CREATE DATABASE <dbname> CHARACTER SET utf8;

Ciao, ti prego di controllare questa domanda stackoverflow.com/questions/46348817/…
Re

Nel mio caso, il nostro db viene creato dalla finestra mobile, quindi per risolvere il problema ho aggiunto quanto segue al comando db: command: istruzione nel mio file di composizione:- --character-set-server=utf8
followben

1
Così semplice. Grazie @Vanuan
Enku il

se questo non è un nuovo progetto, otteniamo il backup da db, lo rilasciamo e lo ricreamo con il set di caratteri utf8 e quindi ripristiniamo il backup. L'ho fatto nel mio progetto che non era nuovo ...
Mohammad Reza,

8

Ho appena escogitato un metodo per evitare errori di cui sopra.

Salva nel database

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

È questo l'unico metodo per salvare stringhe del genere in una tabella MySQL e decodificarlo prima del rendering in modelli per la visualizzazione?


12
Sto riscontrando un problema simile, ma non sono d'accordo che questa sia una soluzione valida. Quando .encode('unicode_escape')in realtà non stai memorizzando caratteri unicode nel database. Stai forzando tutti i client a decodificare prima di usarli, il che significa che non funzionerà correttamente con django.admin o qualsiasi altra cosa.
muudscope,

3
Mentre sembra disgustoso memorizzare codici di escape anziché caratteri, questo è probabilmente uno dei pochi modi per salvare caratteri UTF-8 a 4 byte come le emoji nel utf8set di caratteri a 3 byte di MySQL 5.1 .
markpasc,

2
Esiste una codifica chiamata utf8mb4che consente di memorizzare più del piano multilingue di base. Lo so, penseresti che "UTF8" sia tutto ciò che serve per memorizzare Unicode in modo completo. Beh, lo so, non lo è. Vedi dev.mysql.com/doc/refman/5.5/it/charset-unicode-utf8mb4.html
Mihai Danila,

@jack potresti prendere in considerazione la possibilità di cambiare la risposta accettata con una più utile
Donturner

è una soluzione fattibile, ma non consiglio di usarlo troppo (come sostenuto da @muudscope). Non riesco ancora a memorizzare, ad esempio, le emoji nei database mysql. Qualcuno l'ha realizzato?
Marcelo Sardelich,

6

È possibile modificare le regole di confronto del campo di testo in UTF8_general_ci e il problema verrà risolto.

Nota, questo non può essere fatto in Django.


1

Non stai cercando di salvare stringhe unicode, stai cercando di salvare le stringhe secondarie nella codifica UTF-8. Rendili effettivi letterali stringa unicode:

user.last_name = u'Slatkevičius'

o (quando non hai letterali stringa) decodificali usando la codifica utf-8:

user.last_name = lastname.decode('utf-8')

@Thomas, ho provato esattamente come hai detto, ma generano ancora gli stessi errori.
Jack

0

Modifica semplicemente il tuo tavolo, non c'è bisogno di nulla. basta eseguire questa query sul database. MODIFICA TABELLA ALTER PER table_nameIL SET DI CARATTERI utf8

funzionerà sicuramente.


0

Miglioramento della risposta @madprops - soluzione come comando di gestione 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()

Spero che questo aiuti chiunque tranne me :)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.