SQLAlchemy 报告 BINARY 列的“无效的 utf8mb4 字符串”

Posted

技术标签:

【中文标题】SQLAlchemy 报告 BINARY 列的“无效的 utf8mb4 字符串”【英文标题】:SQLAlchemy reports "Invalid utf8mb4 character string" for BINARY column 【发布时间】:2016-10-20 08:57:22 【问题描述】:

假设这个 mysql 表模式:

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uuid` binary(16) NOT NULL,
  `email` varchar(255) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `photo` binary(16) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uuid` (`uuid`),
  UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4;

当我使用 SQLAlchemy 连接类中的execute() API 时:

with self.engine.begin() as connection:
  user_uuid = uuid.UUID("...")
  result = connection.execute("SELECT email, name, photo FROM user WHERE uuid=%s", user_uuid.bytes)

如果 UUID 是 F393A167-A919-4B50-BBB7-4AD356E89E6B,则 SQLAlchemy 会打印此警告:

/site-packages/sqlalchemy/engine/default.py:450:警告:无效的 utf8mb4 字符串:'F393A1'

uuid 列是BINARY 列,那么为什么 SQLAlchemy 将此参数视为文本参数而不是二进制参数以及如何防止这种情况发生?

【问题讨论】:

【参考方案1】:

解释和解决方法其实在这个bug report in MySQL:

替换:

cursor.execute(""" 插入user (uuid) 值 (%s) """, my_uuid)

cursor.execute(""" 插入user (uuid) 值(_binary %s) """, my_uuid)

注意下划线。它是“_binary”,而不是“二进制”。 这个“_binary”告诉 MySQL 下面的字符串被解释为二进制,而不是被解释/验证为 utf8。

【讨论】:

【参考方案2】:

这个问题在 Python 3 上不会发生,所以我认为问题在于数据库驱动程序无法区分给定 Python 2 str 类型的字节。

不管怎样,似乎直接使用 SQLAlchemy 核心可以正常工作,大概是因为它直接知道列类型。

from sqlalchemy import MetaData, Table, select

meta = MetaData()
user = Table('user', meta, autoload_with=engine)
result = select([user]).where(user.c.uuid == user_uuid.bytes)

如果你想继续执行一个字符串,你可以像 SQLAlchemy 一样强制转换为 bytesarray:

with self.engine.begin() as connection:
    user_uuid = uuid.UUID("...")
    result = connection.execute(
        "SELECT email, name, photo FROM user WHERE uuid=%s",
        bytearray(user_uuid.bytes))

或者告诉 SQLAlchemy 绑定的参数是什么类型来自动获取这个:

from sqlalchemy import text, bindparam, BINARY

with self.engine.begin() as connection:
    user_uuid = uuid.UUID("...")
    stmt = text("SELECT email, name, photo FROM user WHERE uuid = :uuid")
    stmt = stmt.bindparams(bindparam('uuid', user_uuid.bytes, type_=BINARY))
    result = connection.execute(stmt)

【讨论】:

对于 Python 2 strunicode 的问题,您很可能是正确的。如果你记录 SQLAlchemy Core 发出的 SQL 命令,它有什么用? 它在 Python 2 上使用 bytearray,在 Python 3 上使用 bytes 我的意思是由 SQLAlchemy Core 生成并传递给 MySQL 驱动程序的实际 SQL 是什么(通过在引擎上设置 echo=True 可见)? @Pol 我知道,这就是我使用的。它是SELECT tbl.uuid FROM tbl WHERE tbl.uuid = %(uuid_1)s,唯一的区别是下一个日志行传递的值(bytes vs bytearray 所以它没有将_binary 放在SQL 中。我不确定为什么它会起作用。可能 MySQL 驱动看到bytearray 类型并自动添加...

以上是关于SQLAlchemy 报告 BINARY 列的“无效的 utf8mb4 字符串”的主要内容,如果未能解决你的问题,请参考以下文章

SQLAlchemy - 从自动加载表的内部连接映射列的子集

如何将sqlalchemy中列的默认值设置为关系中列的值?

如何根据列的值过滤 SQLAlchemy 结果?

将字符串插入 SQLAlchemy Unicode 列的正确方法

SQLAlchemy:如何根据其后端有条件地选择列的类型

SQLAlchemy 中 JSON 列的自定义 json 序列化程序