【已解决】SqlAlchemy 插入 MySQL JSON 字段时 None 变为 ‘null‘ 字符串,WHERE IS NULL 失效

在使用 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 字段操作更符合预期。

相关推荐
IT199515 分钟前
Fail2Ban 防护 MySQL 实战|登录失败 3 次,IP 封禁 24 小时
数据库·mysql
夕除21 分钟前
redis--IO 多路复用(select + poll)
数据库·sql
wangchunyu1142 小时前
JAVA CompletableFuture异步编程实战
java·开发语言·python
paopaokaka_luck3 小时前
流浪动物救助与收养系统(DeepSeek AI对话、 ECharts多维分析、救助流程衔接、领养与回访衔接、 捐赠确认与公示、志愿者协作)
java·前端·javascript·人工智能·mysql·echarts·推荐算法
2501_944521594 小时前
Immer 跨端三方库仓颉适配全流程
python·eclipse·django
信誓旦旦的程序猿4 小时前
【Python量化实战宝典 #26】止损与回撤控制手册:移动止损、最大回撤(Python 可跑)
python·1024程序员节·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
2501_944521594 小时前
Deepmerge 跨端三方库仓颉适配全流程
java·python·eclipse·github
lzhdim4 小时前
100条常用SQL语句
数据库·sql·mysql·mybatis
天河归来4 小时前
从原始消息到业务数据:三层数据架构的设计与实践
数据库·数据仓库·架构
Patrick在香港4 小时前
同一行 strftime,两台机器跑出两种日期
python·datetime·时区·locale·日期处理