Peewee 的 ==、!= 等运算符返回 Expression 对象而非布尔值,用于构建参数化 SQL;动态条件应收集 Expression 列表后解包传入 where(),禁用字符串拼接以防 SQL 注入;None 值须用 is_null() 或 >> None,IN/NOT IN 必须用 in_()/not_in()。

Peewee 的 ==、!= 等运算符不返回布尔值,而是生成表达式对象
很多人第一次写 User.select().where(User.name == 'Alice') 时,会下意识觉得 User.name == 'Alice' 这段代码在 Python 层就执行并返回 True 或 False。其实不是——它被 Peewee 重载的 __eq__ 拦截,返回的是一个 Expression 实例,内部封装了字段名、操作符、参数值等元信息,直到真正 .execute() 或迭代时才编译成 SQL 并发送给数据库。
这意味着你可以安全地把条件“存起来”再组合:
- 多个条件用
&(AND)、|(OR)连接,它们返回BooleanClauseList,不是 Python 的and/or - 避免用
if user_input:拼字符串,比如f"WHERE name LIKE '%{user_input}%'"—— 这是 SQL 注入高危区 -
None值要用.is_null(True)或>> None(即User.name >> None),不能直接写User.name == None,否则生成= NULL(永远为 false)
动态 where 条件必须用列表收集 + * 解包,别拼字符串
常见错误是根据请求参数手动拼接 SQL 片段,例如:
where_clause = "1=1"
if name:
where_clause += f" AND name LIKE '%{name}%'"
if age_min:
where_clause += f" AND age >= {age_min}"
query = User.raw(f"SELECT * FROM user WHERE {where_clause}")
这种写法完全绕过 Peewee 的参数化机制,name 或 age_min 一旦含单引号或 SQL 关键字,立刻中招。
正确做法是用 Python 列表收集 Expression 对象,最后解包进 .where():
conditions = []
if name:
conditions.append(User.name % f'%{name}%')
if age_min is not None:
conditions.append(User.age >= age_min)
if conditions:
users = User.select().where(*conditions)
else:
users = User.select()
- 每个
conditions.append(...)添加的都是 Peewee 表达式,不是字符串 -
*conditions把列表展开为.where(cond1, cond2, ...),Peewee 自动用AND连接 - 如果需要 OR,用
reduce(operator.or_, conditions)或显式写(cond1) | (cond2)
IN、NOT IN 和子查询必须用 .in_() / .not_in(),别用 Python 的 in
Python 的 in 操作符在 Peewee 中被重载为 (左移),但新手常误用:
- ❌ 错误:
User.id in [1, 2, 3]→ 返回False(因为User.id是对象,不在 list 里) - ❌ 错误:
User.id → 虽然语法对,但若右边是空列表,会生成 <code>id IN (),PostgreSQL 报错,SQLite 返回空结果 - ✅ 正确:
User.id.in_([1, 2, 3]),空列表时自动跳过该条件;或用User.id.in_(subquery)接子查询 - ✅ NOT IN 同理:
User.id.not_in(subquery),不要写~(User.id.in_(...)),语义不同(NULL 时三值逻辑行为不一致)
子查询本身也得是 Peewee 查询对象,例如:
active_ids = User.select(User.id).where(User.active == True) inactive_users = User.select().where(User.id.not_in(active_ids))
排序、分页、模糊匹配这些操作符容易忽略 NULL 处理
比如 User.select().order_by(User.created_at.desc()),当 created_at 为 NULL 时,不同数据库默认排序位置不同(MySQL 放最后,PostgreSQL 放最前)。如果业务要求 NULL 排末尾,得显式写:
User.select().order_by(User.created_at.desc().nulls_last())
-
.nulls_first()/.nulls_last()是 PostgreSQL 和 SQLite 3.30+ 支持的,MySQL 不支持,需换用fn.IF或COALESCE模拟 - 模糊匹配用
%(LIKE)或**(ILIKE),但通配符必须由你控制,别让用户直接输%或_—— 应先user_input.replace('%', '\%').replace('_', '\_')再加包裹 - 分页用
.paginate(page, page_size)安全,但注意它底层是LIMIT OFFSET,大数据量时性能差,真要深分页得改用游标(cursor-based)方案
最常被忽略的一点:所有涉及用户输入的字段名、表名、排序方向('asc' / 'desc')都不能直接插进查询链——它们属于 SQL 标识符,无法参数化,必须白名单校验或映射转换。

















