Błąd MySQL „nieprawidłowa wartość ciągu” podczas zapisywania ciągu znaków Unicode w Django

158

Otrzymałem dziwny komunikat o błędzie, gdy próbowałem zapisać first_name, last_name w modelu auth_user Django.

Nieudane przykłady

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

Udane przykłady

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

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

Zestaw znaków i sortowanie w tabeli

Tabela auth_user ma zestaw znaków utf-8 z sortowaniem utf8_general_ci.

Wyniki polecenia UPDATE

Nie spowodowało to żadnego błędu podczas aktualizacji powyższych wartości do tabeli auth_user za pomocą polecenia 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

Wymienione powyżej wartości, które nie powiodły się, można zaktualizować do tabeli PostgreSQL po przełączeniu bazy danych w Django. To dziwne.

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

Ale z http://www.postgresql.org/docs/8.1/interactive/multibyte.html znalazłem:

Name Bytes/Char
UTF8 1-4

Czy oznacza to, że znak Unicode ma maksymalnie 4 bajty w PostgreSQL, ale 3 bajty w MySQL, co spowodowało powyższy błąd?

Jacek
źródło
2
To problem MySQL, a nie Django: stackoverflow.com/questions/1168036/ ...
Vanuan

Odpowiedzi:

140

Żadna z tych odpowiedzi nie rozwiązała problemu za mnie. Podstawową przyczyną jest:

Nie można przechowywać 4-bajtowych znaków w MySQL z zestawem znaków utf-8.

MySQL ma limit 3 bajtów na znaki utf-8 (tak, jest głupi, ładnie podsumowany przez programistę Django tutaj )

Aby rozwiązać ten problem, musisz:

  1. Zmień swoją bazę danych, tabelę i kolumny MySQL, aby używać zestawu znaków utf8mb4 (dostępne tylko od MySQL 5.5 i nowszych )
  2. Określ zestaw znaków w pliku ustawień Django, jak poniżej:

settings.py

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

Uwaga: podczas ponownego tworzenia bazy danych możesz napotkać problem „ Określony klucz był za długi ”.

Najbardziej prawdopodobną przyczyną jest CharFielddługość maksymalna równa 255 i jakiś indeks (np. Unique). Ponieważ utf8mb4 zużywa o 33% więcej miejsca niż utf-8, musisz zmniejszyć te pola o 33%.

W takim przypadku zmień max_length z 255 na 191.

Alternatywnie możesz edytować konfigurację MySQL, aby usunąć to ograniczenie, ale nie bez hakera django

AKTUALIZACJA: Ponownie napotkałem ten problem i skończyłem przełączając się na PostgreSQL, ponieważ nie byłem w stanie zredukować moich VARCHARznaków do 191.

donturner
źródło
13
ta odpowiedź wymaga więcej głosów pozytywnych. Dzięki! Prawdziwym problemem jest to, że Twoja aplikacja może działać dobrze przez lata, dopóki ktoś nie spróbuje wprowadzić 4-bajtowego znaku.
Michael Bylstra
2
To jest absolutnie właściwa odpowiedź. Ustawienie OPTIONS ma kluczowe znaczenie dla dekodowania znaków emoji przez django i przechowywania ich w MySQL. Sama zmiana zestawu znaków mysql na utf8mb4 za pomocą poleceń SQL nie wystarczy!
Xerion
Nie ma potrzeby aktualizowania zestawu znaków całej tabeli do utf8mb4. Po prostu zaktualizuj zestaw znaków niezbędnych kolumn. Również 'charset': 'utf8mb4'opcja w ustawieniach Django jest krytyczna, jak powiedział @Xerion. Wreszcie problem z indeksem to bałagan. Usuń indeks na kolumnie lub ustaw jego długość nie większą niż 191 lub użyj TextFieldzamiast tego!
Rockallite,
2
Uwielbiam twój link do tego cytatu: to tylko kolejny przypadek celowego i nieodwracalnego uszkodzenia mózgu MySQL. :)
Qback
120

Miałem ten sam problem i rozwiązałem go, zmieniając zestaw znaków w kolumnie. Mimo że baza danych ma domyślny zestaw znaków utf-8, myślę, że kolumny bazy danych mogą mieć inny zestaw znaków w MySQL. Oto zapytanie SQL, którego użyłem:

    ALTER TABLE database.table MODIFY COLUMN col VARCHAR(255)
    CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;
gerdemb
źródło
14
Ugh, zmieniłem wszystkie zestawy znaków na wszystko, co mogłem, dopóki naprawdę nie przeczytałem ponownie tej odpowiedzi: kolumny mogą mieć własne zestawy znaków, niezależnie od tabel i bazy danych. To szalone i też był dokładnie mój problem.
markpasc
1
To zadziałało również dla mnie, używając mysql z domyślnymi ustawieniami w modelu TextField.
madprops
To rozwiązało mój problem. Jedyną zmianą, jaką zrobiłem, było użycie utf8mb4 i utf8mb4_general_ci zamiast utf8 / utf8_general_ci.
Michał Przysucha
70

Jeśli masz ten problem, oto skrypt w Pythonie, który automatycznie zmienia wszystkie kolumny bazy danych 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
źródło
4
To rozwiązanie rozwiązało wszystkie moje problemy z aplikacją django, która przechowywała ścieżki plików i katalogów. Wrzuć dbname jako bazę danych django i pozwól jej działać. Działał jak urok!
Chris,
1
Ten kod nie działał dla mnie, dopóki nie dodałem db.commit()wcześniej db.close().
Mark Erdmann
1
Czy to rozwiązanie pozwala uniknąć problemu omawianego w komentarzu @markpasc: `` ... 4-bajtowe znaki UTF-8, takie jak emoji w 3-bajtowym zestawie znaków utf8 MySQL 5.1 ''
CatShoes
rozwiązanie pomogło mi, kiedy kasowałem rekord przez administratora django, nie miałem żadnego problemu podczas tworzenia o edycji ... dziwne! Udało mi się nawet usunąć bezpośrednio w db
Javier Vieira
Czy powinienem to robić za każdym razem, gdy zmieniam model?
Vanuan
25

Jeśli jest to nowy projekt, po prostu upuściłbym bazę danych i utworzył nową z odpowiednim zestawem znaków:

CREATE DATABASE <dbname> CHARACTER SET utf8;
Vanuan
źródło
Cześć, uprzejmie pomóż, sprawdź to pytanie stackoverflow.com/questions/46348817/…
King
W moim przypadku nasza baza danych jest tworzona przez docker, więc aby naprawić, dodałem do polecenia db: polecenie: instrukcja w moim pliku tworzenia:- --character-set-server=utf8
followben
1
Tak proste jak to. Dzięki @ Vanuan
Enku
jeśli nie jest to nowy projekt, otrzymujemy kopię zapasową z db, upuszczamy ją i odtwarzamy z zestawem znaków utf8, a następnie przywracamy kopię zapasową. Zrobiłem to w moim projekcie, który nie był nowy ...
Mohammad Reza
8

Właśnie wymyśliłem jedną metodę, aby uniknąć powyższych błędów.

Zapisz do bazy danych

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

Czy jest to jedyna metoda zapisywania takich łańcuchów w tabeli MySQL i dekodowania ich przed renderowaniem do szablonów w celu wyświetlenia?

Jacek
źródło
12
Mam podobny problem, ale nie zgadzam się, że jest to prawidłowe rozwiązanie. Kiedy w .encode('unicode_escape')rzeczywistości nie przechowujesz znaków Unicode w bazie danych. Zmuszasz wszystkich klientów do odkodowania przed ich użyciem, co oznacza, że ​​nie będzie to działać poprawnie z django.admin lub wieloma innymi rzeczami.
muudscope
3
Chociaż przechowywanie kodów ucieczki zamiast znaków wydaje się niesmaczne, jest to prawdopodobnie jeden z niewielu sposobów zapisywania 4-bajtowych znaków UTF-8, takich jak emoji, w 3-bajtowym utf8zestawie znaków MySQL 5.1 .
markpasc
2
Istnieje kodowanie, utf8mb4które pozwala na przechowywanie czegoś więcej niż tylko Basic Multilingual Plane. Wiem, można by pomyśleć, że „UTF8” to wszystko, co jest potrzebne do pełnego przechowywania Unicode. Cóż, co wiesz, tak nie jest. Zobacz dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
Mihai Danila
@jack, możesz rozważyć zmianę zaakceptowanej odpowiedzi na taką, która jest bardziej przydatna
donturner
jest to możliwe obejście, ale nie polecam go również używać (zgodnie z zaleceniami @muudscope). Nadal nie mogę przechowywać np. Emoji w bazach danych mysql. Czy ktoś to osiągnął?
Marcelo Sardelich
6

Możesz zmienić sortowanie swojego pola tekstowego na UTF8_general_ci, a problem zostanie rozwiązany.

Zauważ, że nie można tego zrobić w Django.

Wei An
źródło
1

Nie próbujesz zapisywać ciągów znaków Unicode, tylko próbujesz zapisać bajtesty w kodowaniu UTF-8. Ustaw je jako rzeczywiste literały znaków Unicode:

user.last_name = u'Slatkevičius'

lub (jeśli nie masz literałów łańcuchowych) dekoduj je za pomocą kodowania utf-8:

user.last_name = lastname.decode('utf-8')
Thomas Wouters
źródło
@ Thomas, próbowałem dokładnie tak, jak powiedziałeś, ale nadal powoduje to te same błędy.
jack
0

Po prostu zmień swój stół, nie musisz nic robić. po prostu uruchom to zapytanie w bazie danych. ALTER TABLE table_nameCONVERT TO CHARACTER SET utf8

to na pewno zadziała.

Rishabh Jhalani
źródło
0

Ulepszenie @madprops answer - rozwiązanie jako polecenie zarządzania 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()

Mam nadzieję, że to pomoże nikomu oprócz mnie :)

Ron
źródło