在使用 SqlAlchemy 操作 MySQL 数据库的过程中,当向 JSON 类型的字段插入None值时,数据库中最终存储的不是 MySQL 原生的NULL,而是字符串'null'。这个问题会导致后续查询(比如WHERE IS NULL)完全失效,给数据处理带来诸多麻烦。本文将详细分析问题原因,并给出简洁有效的解决方案。
一、问题复现:None 变成了 'null' 字符串
先通过一段简单的代码复现这个问题,让你直观看到现象:
python
import asyncio
from sqlalchemy import Column, Integer, String, JSON, select
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
DB_URL = "mysql+asyncmy://root:root@localhost:3306/test_db?charset=utf8mb4"
async_engine = create_async_engine(DB_URL, echo=True)
AsyncSession = async_sessionmaker(bind=async_engine, expire_on_commit=False)
class Base(DeclarativeBase):
__abstract__ = True
# 定义模型
class TestJsonTable(Base):
__tablename__ = "test_json_table"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(50))
json_data = Column(JSON)
async def start():
# 创建数据表
async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# 插入测试数据
async with AsyncSession() as session:
async with session.begin():
test_data = TestJsonTable(name="测试数据", json_data=None)
session.add(test_data)
result = await session.execute(select(TestJsonTable).where(TestJsonTable.json_data== 'null'))
print(*(e.json_data for e in result.scalars().all()), sep='\n')
if __name__ == '__main__':
asyncio.run(start())
运行代码后,可以发现在控制台输出的 SQL 执行日志中,插入测试数据 SQL 的字段会填充为'null'而非None:

数据库中json_data字段存储的也是字符串'null',而非 MySQL 的NULL;

执行SELECT * FROM test_json_table WHERE json_data IS NULL会返回空结果,因为字段值是字符串而非真正的 NULL。

但是在代码中查询返回的result.json_data是None:

二、问题原因:none_as_null 参数默认值为 False
首先要明确两个容易混淆的概念,这是解决问题的关键:
| 类型 | 数据库存储形态 | SqlAlchemy 查询结果 | WHERE 字段 IS NULL 是否生效 |
|---|---|---|---|
| MySQL 原生 NULL | 空(无值) | None | ✅ 生效 |
| JSON 格式的 null | JSON 格式的 null | None | ❌ 不生效 |
数据库中 JSON 字段实际存储的是字符串'null'(JSON 格式的 null),但通过 SqlAlchemy 查询后打印该字段却显示为None,这是因为 SqlAlchemy 对 JSON 类型的解析逻辑导致的「存储形态」和「查询结果」不一致。
SqlAlchemy 在读取时会按照 JSON 规范,把 JSON 的null解析为 Python 的None,所以打印出来是None,但数据库里实际存储的并不是 MySQL 原生的 NULL。
SqlAlchemy 的JSON和JSONB列类型(MySQL 中JSONB是JSON的别名)有一个关键参数none_as_null:
- 默认值 :
False,此时会将 Python 的None转换为字符串'null'插入数据库; - 设置为 True :会将 Python 的
None转换为 MySQL 原生的NULL值插入。
这个参数的设计是为了兼容不同数据库的 JSON 类型处理逻辑,但 MySQL 场景下默认值会导致不符合预期的结果。
三、解决方案:初始化 JSON 字段时设置 none_as_null=True
在定义 sqlalchemy 模型 JSON 列时,显式设置none_as_null=True即可解决问题,修改后的核心代码如下:
python
class TestJsonTable(Base):
__tablename__ = "test_json_table"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(50))
# 关键修改:设置none_as_null=True
json_data = Column(JSON(none_as_null=True))
JSON(none_as_null=True) 是关键,它会让 SqlAlchemy 将 Python 的None转换为 MySQL 原生 NULL 插入,而非 JSON 的'null'字符串。
再次执行代码,

数据库中json_data字段存储的是 MySQL 原生的NULL:

执行SELECT * FROM test_json_table WHERE json_data IS NULL能正确返回数据:

代码中查询返回的result.json_data是None,类型为NoneType;
这个问题虽然隐蔽,但解决方法非常简单,只需要在定义列时多添加一个参数即可。希望本文能帮助你避开这个坑,让 SqlAlchemy 的 JSON 字段操作更符合预期。