SQLAlchemy如何实现JSONB类型字典字段的属性值过滤查询
问题描述
表结构如下:
+-------------------------------------------------------------+ | id | res_id | path | overrides | |-------------------------------------------------------------| | 1 | res_1 | res_1 | {"enabled": True} | | 2 | res_1.1 | res_1.res_1.1 | {"enabled": False}| | 3 | res_1.2 | res_1.res_1.2 | {"enabled": False}| | 4 | res_1.1.1 | res_1.res_1.1.res_1.1.1| {"enabled": False}| +-------------------------------------------------------------+
- 表中
overrides字段为JSONB类型,用于标记继承关系中断点:正常查询res_1.1.1时,按照继承逻辑会同时返回路径上关联的id=1、id=2和自身id=4的资源;但id=1的记录overrides.enabled值为True,代表该节点不会向下传递继承关系,因此该场景预期仅返回id=2、id=4的记录。 - 尝试实现「过滤掉
overrides字段中enabled值为True的记录」时,采用.filter(MyModel.overrides["enabled"] is not True)写法未生效;尝试调用字典原生的get()、values()、keys()方法时,抛出AttributeError: Neither 'InstrumentedAttribute' object nor 'Comparator' object associated with MyModel.overrides has an attribute 'get'错误,无法实现过滤逻辑。
错误原因
is not是Python原生的身份判断运算符,在传入filter()方法前就会被Python解释器直接求值为布尔值,无法被SQLAlchemy解析为数据库层面的查询条件,自然不会生效。- 在模型类层面访问
MyModel.overrides拿到的是ORM字段描述符(InstrumentedAttribute对象),不是从数据库加载完成后反序列化得到的Python字典,因此无法直接调用字典的get()、keys()等原生方法——这些方法只能在记录被查询加载到内存后,在实例对象的字段值上调用,无法用于生成数据库层面的过滤SQL。
正确实现方式
SQLAlchemy针对PostgreSQL的JSONB类型提供了专属的比较操作符,不需要手动加载所有记录到内存过滤,直接在数据库层即可完成条件判断:
- 基础过滤写法(适用于SQLAlchemy 1.3+所有版本)
from sqlalchemy import Boolean, true # 过滤掉overrides.enabled为True的记录,兼容键不存在的场景 query = MyModel.query.filter( # 当enabled键不存在时,取值为SQL null,与true()比较结果为不相等,会被保留 MyModel.overrides["enabled"].astext.cast(Boolean) != true() )
- 更简洁的写法(适用于SQLAlchemy 2.0+,或1.4版本开启2.0兼容模式)
SQLAlchemy 2.0为JSON类型提供了直接的类型转换方法,不需要手动调用cast:
from sqlalchemy import select valid_records = db.session.execute( select(MyModel).where(MyModel.overrides["enabled"].as_boolean() != True) ).scalars().all()
- 结合继承路径查询场景的完整示例
查询res_1.1.1关联的有效继承节点时,可以直接将过滤条件和路径前缀匹配条件组合:
target_path = "res_1.res_1.1.res_1.1.1" valid_nodes = MyModel.query.filter( # 匹配所有当前路径的前缀节点(含自身) target_path.like(f"{MyModel.path}%"), # 排除开启了继承中断的节点 MyModel.overrides["enabled"].astext.cast(Boolean) != True ).all()
执行上述查询后,id=1的记录会因为overrides.enabled=True被过滤,最终返回id=2、id=4的记录,符合预期。
注意:如果表中存在
overrides字段本身为NULL、或者overrides里没有enabled键的记录,上述写法会自动保留这些记录——因为JSON路径取值不存在时会返回SQL NULL,和True比较的结果为不相等,刚好符合「仅当明确设置enabled=True时才中断继承」的业务逻辑。
内容的提问来源于stack exchange,提问作者lr_optim
相关产品推荐
相关产品推荐

