MySQL-Fehler "Falscher Zeichenfolgenwert" beim Speichern der Unicode-Zeichenfolge in Django

158

Beim Versuch, Vorname und Nachname in Djangos auth_user-Modell zu speichern, wurde eine seltsame Fehlermeldung angezeigt.

Beispiele fehlgeschlagen

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

Erfolgreiche Beispiele

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

MySQL-Einstellungen

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)

Tabellenzeichensatz und Sortierung

Die Tabelle auth_user enthält den Zeichensatz utf-8 mit der Sortierung utf8_general_ci.

Ergebnisse des UPDATE-Befehls

Beim Aktualisieren der obigen Werte in die Tabelle auth_user mit dem Befehl UPDATE wurde kein Fehler ausgegeben.

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

Die oben aufgeführten fehlgeschlagenen Werte können in die PostgreSQL-Tabelle aktualisiert werden, als ich das Datenbank-Backend in Django gewechselt habe. Es ist komisch.

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

Aber von http://www.postgresql.org/docs/8.1/interactive/multibyte.html fand ich Folgendes:

Name Bytes/Char
UTF8 1-4

Bedeutet dies, dass Unicode-Zeichen in PostgreSQL maximal 4 Byte und in MySQL maximal 3 Byte haben, was den oben genannten Fehler verursacht hat?

Jack
quelle
2
Es ist ein MySQL-Problem, nicht Django: stackoverflow.com/questions/1168036/…
Vanuan

Antworten:

139

Keine dieser Antworten löste das Problem für mich. Die Hauptursache ist:

Sie können keine 4-Byte-Zeichen in MySQL mit dem Zeichensatz utf-8 speichern.

MySQL hat ein 3-Byte-Limit für utf-8-Zeichen (ja, es ist verrückt, hier von einem Django-Entwickler gut zusammengefasst )

Um dies zu lösen, müssen Sie:

  1. Ändern Sie Ihre MySQL-Datenbank, -Tabelle und -Spalten, um den Zeichensatz utf8mb4 zu verwenden (nur ab MySQL 5.5 verfügbar).
  2. Geben Sie den Zeichensatz in Ihrer Django-Einstellungsdatei wie folgt an:

settings.py

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

Hinweis: Bei der Neuerstellung Ihrer Datenbank tritt möglicherweise das Problem " Angegebener Schlüssel war zu lang " auf.

Die wahrscheinlichste Ursache ist eine CharFieldmit einer maximalen Länge von 255 und einer Art Index (z. B. eindeutig). Da utf8mb4 33% mehr Speicherplatz als utf-8 benötigt, müssen Sie diese Felder um 33% verkleinern.

Ändern Sie in diesem Fall die max_length von 255 auf 191.

Alternativ können Sie Ihre MySQL-Konfiguration bearbeiten, um diese Einschränkung zu beseitigen, jedoch nicht ohne Django-Hackery

UPDATE: Ich bin gerade wieder auf dieses Problem gestoßen und bin zu PostgreSQL gewechselt, weil ich meine VARCHARAnzahl auf 191 Zeichen nicht reduzieren konnte .

donturner
quelle
12
Diese Antwort braucht viel, viel, viel mehr Stimmen. Vielen Dank! Das eigentliche Problem ist, dass Ihre Anwendung möglicherweise jahrelang einwandfrei läuft, bis jemand versucht, ein 4-Byte-Zeichen einzugeben.
Michael Bylstra
2
Dies ist absolut die richtige Antwort. Die Einstellung OPTIONS ist wichtig, damit Django Emoji-Zeichen dekodiert und in MySQL speichert. Es reicht nicht aus, den MySQL-Zeichensatz über SQL-Befehle in utf8mb4 zu ändern!
Xerion
Der Zeichensatz der gesamten Tabelle muss nicht auf utf8mb4 aktualisiert werden. Aktualisieren Sie einfach den Zeichensatz der erforderlichen Spalten. Auch die 'charset': 'utf8mb4'Option in den Django-Einstellungen ist kritisch, wie @Xerion sagte. Schließlich ist das Indexproblem ein Chaos. Entfernen Sie den Index für die Spalte oder machen Sie eine Länge von nicht mehr als 191 oder verwenden Sie TextFieldstattdessen ein!
Rockallite
1
Ich liebe Ihren Link zu diesem Zitat: Dies ist nur ein weiterer Fall, in dem MySQL absichtlich und irreversibel durch das Gehirn geschädigt wird. :)
Qback
120

Ich hatte das gleiche Problem und löste es durch Ändern des Zeichensatzes der Spalte. Obwohl Ihre Datenbank einen Standardzeichensatz von hat utf-8, ist es meiner Meinung nach möglich, dass Datenbankspalten in MySQL einen anderen Zeichensatz haben. Hier ist die SQL-Abfrage, die ich verwendet habe:

    ALTER TABLE database.table MODIFY COLUMN col VARCHAR(255)
    CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;
gerdemb
quelle
14
Ugh, ich habe alle Zeichensätze geändert, bis ich diese Antwort wirklich wieder gelesen habe: Spalten können ihre eigenen Zeichensätze haben, unabhängig von den Tabellen und der Datenbank. Das ist verrückt und war auch genau mein Problem.
Markpasc
1
Dies funktionierte auch für mich, indem ich MySQL mit den Standardeinstellungen in einem TextField-Modell verwendete.
Madprops
Dies löste mein Problem. Die einzige Änderung, die ich vorgenommen habe, war die Verwendung von utf8mb4 und utf8mb4_general_ci anstelle von utf8 / utf8_general_ci.
Michal Przysucha
70

Wenn Sie dieses Problem haben, finden Sie hier ein Python-Skript, mit dem Sie alle Spalten Ihrer MySQL-Datenbank automatisch ändern können.

#! /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
quelle
4
Diese Lösung löste alle meine Probleme mit einer Django-App, die Datei- und Verzeichnispfade speicherte. Geben Sie dbname als Ihre Django-Datenbank ein und lassen Sie sie laufen. Lief wie am Schnürchen!
Chris
1
Dieser Code hat bei mir nicht funktioniert, bis ich ihn db.commit()zuvor hinzugefügt habe db.close().
Mark Erdmann
1
Vermeidet diese Lösung das in @markpasc kommentierte Problem: '... 4-Byte-UTF-8-Zeichen wie Emoji im 3-Byte-utf8-Zeichensatz von MySQL 5.1'
CatShoes
Die Lösung half mir, als ich einen Datensatz durch Django Admin löschte. Ich hatte kein Problem beim Erstellen oder Bearbeiten ... komisch! Ich konnte sogar direkt in der Datenbank löschen
Javier Vieira
Sollte ich dies jedes Mal tun, wenn ich das Modell ändere?
Vanuan
25

Wenn es sich um ein neues Projekt handelt, lösche ich einfach die Datenbank und erstelle ein neues mit einem geeigneten Zeichensatz:

CREATE DATABASE <dbname> CHARACTER SET utf8;
Vanuan
quelle
Hallo, bitte
King
In meinem Fall wird unsere Datenbank von Docker erstellt. Um dies zu beheben, habe ich der Anweisung db: command: in meiner - --character-set-server=utf8
Erstellungsdatei
1
So einfach ist das. Dank @Vanuan
Enku
Wenn dies kein neues Projekt ist, erhalten wir ein Backup von db, löschen es und erstellen es mit dem Zeichensatz utf8 neu und stellen dann das Backup wieder her. Ich habe es in meinem Projekt gemacht, das nicht neu war ...
Mohammad Reza
8

Ich habe nur eine Methode gefunden, um die oben genannten Fehler zu vermeiden.

In Datenbank speichern

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

Ist dies die einzige Methode, um solche Zeichenfolgen in einer MySQL-Tabelle zu speichern und zu dekodieren, bevor sie zur Anzeige in Vorlagen gerendert werden?

Jack
quelle
12
Ich habe ein ähnliches Problem, aber ich stimme nicht zu, dass dies eine gültige Lösung ist. Wenn Sie .encode('unicode_escape')keine Unicode-Zeichen in der Datenbank speichern. Sie zwingen alle Clients, den Code zu entschlüsseln, bevor Sie sie verwenden. Dies bedeutet, dass dies mit django.admin oder allen möglichen anderen Dingen nicht ordnungsgemäß funktioniert.
Muudscope
3
Während es unangenehm erscheint, Escape-Codes anstelle von Zeichen zu speichern, ist dies wahrscheinlich eine der wenigen Möglichkeiten, 4-Byte-UTF-8-Zeichen wie Emoji im 3-Byte- utf8Zeichensatz von MySQL 5.1 zu speichern .
Markpasc
2
Es gibt eine Codierung namens utf8mb4, mit der mehr als die mehrsprachige Grundebene gespeichert werden kann. Ich weiß, Sie würden denken, "UTF8" ist alles, was benötigt wird, um Unicode vollständig zu speichern. Nun, was weißt du, das ist es nicht. Siehe dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
Mihai Danila
@jack Vielleicht möchten Sie die akzeptierte Antwort in eine nützlichere
ändern
Es ist eine praktikable Problemumgehung, aber ich empfehle nicht, sie auch zu verwenden (wie von @muudscope empfohlen). Ich kann beispielsweise Emoji immer noch nicht in MySQL-Datenbanken speichern. Hat es jemand geschafft?
Marcelo Sardelich
6

Sie können die Sortierung Ihres Textfelds in UTF8_general_ci ändern, und das Problem wird behoben.

Beachten Sie, dass dies in Django nicht möglich ist.

Wei An
quelle
1

Sie versuchen nicht, Unicode-Zeichenfolgen zu speichern, sondern Bytestrings in der UTF-8-Codierung zu speichern. Machen Sie sie zu tatsächlichen Unicode-String-Literalen:

user.last_name = u'Slatkevičius'

oder (wenn Sie keine String-Literale haben) dekodieren Sie sie mit der utf-8-Codierung:

user.last_name = lastname.decode('utf-8')
Thomas Wouters
quelle
@ Thomas, ich habe genau das versucht, was du gesagt hast, aber es wirft immer noch die gleichen Fehler auf.
Jack
0

Ändern Sie einfach Ihren Tisch, Sie brauchen nichts. Führen Sie diese Abfrage einfach in der Datenbank aus. ALTER TABLE table_nameCONVERT TO CHARACTER SET utf8

es wird definitiv funktionieren.

Rishabh Jhalani
quelle
0

Verbesserung der Antwort von @madprops - Lösung als Django-Verwaltungsbefehl:

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()

Hoffe das hilft jedem außer mir :)

Ron
quelle