我可以安全地将 utf8mb4 连接与 utf8 列一起使用吗?

Can I safely use a utf8mb4 connection with utf8 columns?

我有一些带有 utf8mb4 字段的 MySQL 表,还有一些带有 utf8.

的表

在所有表的 PDO 连接字符串中使用 utf8mb4 是否安全?或者我是否必须将所有内容都转换为 utf8mb4,或者启动两个不同的 PDO 连接?


编辑:问题不是 "can I store 4-byte characters into utf8 columns?" 我们已经知道我们不能,这不取决于连接,所以如果列是 utf8 它表示它将不会接收 4 字节字符,例如国家或货币代码、电子邮件地址、用户名...其中输入由应用程序验证。

简答:,不安全。

如果您的数据有 utf8mb4 个字符,并且您使用的是 MySQL utf8 字符集连接,您将 运行 遇到问题,因为 MySQL utf8 字符集仅支持 BMP 字符(最多 3 个字节的字符)。

我的建议是将所有表格转换为 utf8mb4 以获得完整的 UTF-8 支持。此外,utf8mb4 向后兼容 utf8

这可以使用以下脚本很容易地进行测试:

<?php

$pdo = new PDO('mysql:host=localhost;dbname=test', 'test', '');

$pdo->exec("
    drop table if exists utf8_test;
    create table utf8_test(
        conn varchar(50) collate ascii_bin,
        column_latin1  varchar(50) collate latin1_general_ci,
        column_utf8    varchar(50) collate utf8_unicode_ci,
        column_utf8mb4 varchar(50) collate utf8mb4_unicode_ci
    );
");

$latin = 'abc äŒé';
$utf8  = '♔♕';
$mb4   = ' ';

$pdo->exec("set names utf8");

$pdo->exec("
    insert into utf8_test(conn, column_latin1, column_utf8, column_utf8mb4)
     values ('utf8', '$latin', '$latin $utf8', '$latin $utf8 $mb4')
");

$pdo->exec("set names utf8mb4");

$pdo->exec("
    insert into utf8_test(conn, column_latin1, column_utf8, column_utf8mb4)
     values ('utf8mb4', '$latin', '$latin $utf8', '$latin $utf8 $mb4')
");

$result = $pdo->query('select * from utf8_test')->fetchAll(PDO::FETCH_ASSOC);

var_export($result);

这是结果:

array (
  0 => 
  array (
    'conn' => 'utf8',
    'column_latin1' => 'abc äŒé',
    'column_utf8' => 'abc äŒé ♔♕',
    'column_utf8mb4' => 'abc äŒé ♔♕ ???? ????',
  ),
  1 => 
  array (
    'conn' => 'utf8mb4',
    'column_latin1' => 'abc äŒé',
    'column_utf8' => 'abc äŒé ♔♕',
    'column_utf8mb4' => 'abc äŒé ♔♕  ',
  ),
)

如您所见,当我们使用 utf8mb4 列时,我们不能使用 utf8 作为连接字符集(请参阅 ????)。但是我们可以在使用 utf8 列时使用 utf8mb4 进行连接。写入和读取 latinascii 列也没有问题。

原因是您可以在 utf8mb4 中编码任何 utf8latinascii 字符,但反之则不行。所以在这种情况下使用 utf8mb4 作为连接的字符集是安全的。

简短回答: 是,如果您只使用 3 字节(或更短)的 UTF-8 字符。

或者... 如果您打算使用 4 字节 UTF-8 字符,例如 .

长答案:

(我会解释为什么 "no" 可能 是正确的答案。)

连接确定客户端使用的编码。

列上的 CHARACTER SET(或者,默认情况下,来自 table)确定可以将什么编码放入该列。

CHARACTER SET utf8utf8mb4 的子集。也就是说,acceptable到utf8(通过连接或列)的所有字符都是acceptable到utf8mb4。换句话说,MySQL 的 utf8mb4(与外界的 UTF-8 相同)具有完整的 4 字节 utf-8 编码,比 MySQL 最多 3 个字节 utf8(又名 "BMP")

(从技术上讲,utf8mb4 最多只能处理 4 个字节,但 UTF-8 可以处理更长的字符。但是,我怀疑我有生之年是否会出现 5 个字节的字符。)

因此,如果连接是 utf8mb4 并且 tables 中的列仅为 utf8,那么客户端中任何 3 字节(或更短的)UTF-8 字符都会发生这种情况:每个字符进出服务器没有转换,没有错误。注意:问题出现在INSERT,而不是SELECT;但是,在执行 SELECT.

之前,您可能不会注意到问题

但是,如果客户端中有表情符号怎么办?现在你会得到一个错误。 (或 t运行 字符串)(或问号)这是因为 4 字节的表情符号(例如,)不能被压缩到 3 字节的 "utf8" (或“1 字节latin1" 或 ...).

如果您运行宁 5.5 或 5.6,您可能 运行 进入 767(或 191)问题。我在 here 中提供了几种解决方法。 None 完美。

至于反转(utf8 连接但 utf8mb4 列):如果您设法将一些 4 字节字符放入 table,SELECT 可能会遇到麻烦。

"Official sources" -- 祝你好运。我花了十年时间试图梳理角色处理的来龙去脉,然后将其简化为可操作的句子。那段时间大部分时间都在想我已经有了所有的答案,结果却遇到了另一个失败的测试用例。 中列出了常见情况。然而,这并没有直接解决你的问题!

来自评论

mysql> SHOW CREATE TABLE emoji\G
*************************** 1. row ***************************
       Table: emoji
Create Table: CREATE TABLE `emoji` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `text` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql> insert into emoji (text) values ("abc");
Query OK, 1 row affected (0.01 sec)

mysql> show variables like 'char%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | utf8                       |
| character_set_connection | utf8                       |
| character_set_database   | utf8mb4                    |
| character_set_filesystem | binary                     |
| character_set_results    | utf8                       |
| character_set_server     | utf8mb4                    |
| character_set_system     | utf8                       |
| character_sets_dir       | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.00 sec)

上面说 "connection"(认为 "client")使用的是 utf8,而不是 utf8mb4。

mysql> insert into emoji (text) values ("");  -- 4-byte Emoji
Query OK, 1 row affected, 1 warning (0.00 sec)

mysql> show warnings;
+---------+------+----------------------------------------------------------------------------------+
| Level   | Code | Message                                                                          |
+---------+------+----------------------------------------------------------------------------------+
| Warning | 1366 | Incorrect string value: '\xF0\x9F\x98\x85\xF0\x9F...' for column 'text' at row 1 |
+---------+------+----------------------------------------------------------------------------------+
1 row in set (0.00 sec)

现在,将 'connection' 更改为 utf8mb4:

mysql> SET NAMES utf8mb4;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into emoji (text) values ("");
Query OK, 1 row affected (0.01 sec)

mysql> SELECT * FROM emoji;
+----+--------------+
| id | text         |
+----+--------------+
|  1 | ? ? ? ?      |
|  2 | abc          |
|  3 | ???????????? |   -- from when "utf8" was in use
|  4 |              |  -- Success with utf8mb4 in use
+----+--------------+
4 rows in set (0.01 sec)