Perl DBI und mysql_enable_utf8

Bytesequenzen und UTF-8-Zeichenketten mit Perl nach MySQL schreiben und lesen

In diesem kleinen Artikel geht es darum, was das Attribut $dbh->{mysql_enable_utf8} = 1; bewirkt. Betrachte untenstehenden CODE:

use strict;
use warnings;

sub say{
    local $, = "\n";
    local $\ = "\n";
    print @_;
}

use dbh;
use insert;
use Encode;

my $m = bless{};
my $dbh = $m->dbh('myweb');
$dbh->{mysql_enable_utf8} = 1;

$dbh->do(q(
    CREATE TABLE IF NOT EXISTS kette(
        zeichen VARCHAR(255) NOT NULL DEFAULT ''
    )charset=UTF8
));

$m->insert($dbh, 'kette', zeichen => '€');
my $r = $dbh->selectrow_hashref(q(SELECT * FROM kette));

say $r->{zeichen}; # Wide character in print at ...

$dbh->do(q(DROP TABLE IF EXISTS kette));

Erläuterungen

Es wird die Verbindung nach MySQL hergestellt, Perl bekommt ein Databasehandle (DBH) und diesem wird mit $dbh->{mysql_enable_utf8} = 1; das entsprechende Attribut vergeben. Das einzufügende Zeichen ist das Eurozeichen was im INSERT-Statement notiert wird. Da die Perl-Scriptdatei mit der Zeichenkodierung UTF-8 gespeichert wurde, betrachtet Perl intern das Eurozeichen nicht als Zeichen sondern als Bytefolge von drei Bytes mit den Wertigkeiten E2 82 AC. Genau diese drei Bytes werden beim INSERT über das Datenbankhandle übertragen.

Merke: Wenn dem INSERT-Statement nicht die Bytesequenz sonderen ein utf8kodiertes Eurozeichen übergeben wurde, bspw. mit

$m->insert($dbh, 'kette', zeichen => decode_utf8 '€');

sorgt das DBH-Attribut $dbh->{mysql_enable_utf8} = 1; dafür, daß bei der Übertragung die Kodierung ausgeschaltet wird. In beiden Fällen also beeinhaltet das vom $dbh erzeugte Payload keine Zeichenketten sondern stets Bytesequenzen. Grundsätzlich gilt: Verlassen Zeichenketten, die perlintern eine bestimmte Kodierung haben das Programm, ist die Kodierung abzuschalten. Nach draußen gehen also nur Bytes bzw. Bytesequenzen.

Als Gegenstück zum INSERT wird nun der String wieder aus der MySQL-Tabelle gelesen. Hierbei sorgt das Attribut mysql_enable_utf8 dafür, daß die vom DBH gelieferten Bytesequenzen automatisch in das perlinterne Format mit der Kodierung UTF-8 umgewandelt werden. Von daher erscheint auch bei der print-Ausgabe die Fehlermeldung Wide character in print at... Für eine fehlerfreie Ausgabe auf STDOUT ist die Kodierung wiederum auszuschalten, zum Beispiel mit:

say encode_utf8 $r->{zeichen};

Warum überhaupt eine Zeichenkodierung

Die Antwort ist ganz einfach: Nur dann, wenn mit Zeichen operiert werden soll, müssen diese in einer bestimmten Kodierung vorliegen. Ansonsten unterscheidet Perl seit v5.6 zwischen Bytesequenzen und kodierten Zeichenketten und arbeitet demzufolge entweder byteorientiert oder zeichenorientiert (Bytesemantic, Charactersemantic). Wenn also Perlfunktionen wie length() oder substr() u.a. Stringfunktionen zeichenorientiert arbeiten sollen, ist die Kodierung einzuschalten. Ansonsten arbeiten diese Funktionen bytesemantisch. So gibt length() entweder die Anzahl der Zeichen oder eben die Anzahl der Bytes zurück. Auch das Verhalten von regulären Ausdrückn ist bestimmt durch diese Arbeitsweise.

Bytesemantische Datenverarbeitung

Wenn also keine Operationen mit Zeichen und den entsprechenden Funktionen nötig sind, kann man sich das Einschalten und Ausschalten der Kodierung auch schenken. Fazit: mysql_enable_utf8 hat mit MySQL überhaupt gar nichts zu tun denn es bewirkt lediglich daß die von MySQL kommenden Strings perlintern utf8kodiert vorliegen.

Set Names UTF8, zeichenorientierte Datenverarbeitung

Das Statement set names utf8 collate utf8_general_ci setzt die Variablen character_set_client, character_set_connection, und character_set_results

mysql> show variables like "character_set%";
+--------------------------+---------------+
| Variable_name            | Value         |
+--------------------------+---------------+
| character_set_client     | utf8          |
| character_set_connection | utf8          |
| character_set_results    | utf8          |
+--------------------------+---------------+

Sofern diese Einstellungen über DBI genutzt werden sollen, kann dies natürlich auch für den PerlClient angewiesen werden:

use strict;
use warnings;

sub say{
    local $, = "\n";
    local $\ = "\n";
    print @_;
}

use dbh;
use insert;
use Encode;

my $m = bless{};
my $dbh = $m->dbh('myweb');
$dbh->{mysql_enable_utf8} = 1;
$dbh->do('set names utf8 collate utf8_general_ci');

$dbh->do(q(
    CREATE TABLE IF NOT EXISTS kette(
        zeichen VARCHAR(255) NOT NULL DEFAULT ''
    )
));

$m->insert($dbh, 'kette', zeichen => 'äöü');
my $r = $dbh->selectrow_hashref(q(SELECT UPPER(zeichen) as zeichen FROM kette));

say $r->{zeichen}; # ÄÖÜ

$dbh->do(q(DROP TABLE IF EXISTS kette));

Erst hiermit lassen sich MySQL-Funktionen die von der Zeichenkodierung abhängig sind nutzen wie obenstehendes Beispiel für die Funktion UPPER() zeigt.

Problem mit UTF8 und Zeichen mit mehr als 3 Bytes

Beim Versuch, mit o.g. Code das Zeichen $m->insert($dbh, 'kette', zeichen => pack "U", 0x1F440); einzufügen erscheint eine Fehlermeldung: DBD::mysql::db do failed: Incorrect string value: '\xF0\x9F\x91\x80' for column 'zeichen' at... Das liegt daran daß MySQL in Sachen UTF-8 weiterhin unterscheidet ob der Anzahl an Bytes welche utf8kodierte Zeichen mit sich bringen. Auf jeden Fall ist es unsinnig, vor einem INSERT set names utf8 collate utf8_general_ci anzuweisen. Wie MySQL mit utf8kodierten Zeichen umgeht die mehr als 3 Bytes haben, siehe Dokumentation.

Merke: Bei einem INSERT ist Bytesemantic angebracht.


Datenschutzerklärung: Diese Seite dient rein privaten Zwecken. Auf den für diese Domäne installierten Seiten werden grundsätzlich keine personenbezogenen Daten erhoben. Das Loggen der Zugriffe mit Ihrer Remote Adresse erfolgt beim Provider soweit das technisch erforderlich ist. nmq​rstx-18­@yahoo.de