本文详解如何在 django 中安全、准确地执行 postgresql 原生 `with recursive` 查询来获取带层级结构的菜单树,重点解决因字段名不匹配导致的 `valueerror: cannot assign "...": "menuentry.parent" must be a "menuentry" instance` 错误。
在 Django 中使用 Model.objects.raw() 执行原生 PostgreSQL 递归查询时,数据库字段名必须与模型字段定义严格对齐,否则 Django ORM 在实例化模型对象时会因外键字段映射失败而抛出 ValueError。你遇到的错误:
ValueError: Cannot assign "'menu_B_1lvl_entry'": "MenuEntry.parent" must be a "MenuEntry" instance.
根本原因在于:你的 CTE 查询中声明了列别名 my_2ndmenu_items (id, parent, text),其中 parent 对应的是数据库中的 parent_id(即外键关联字段的整数值),但 Django 模型中 parent 是一个 ForeignKey 字段——ORM 期望该列返回的是 parent_id 的整数 ID 值,且列名必须为 parent_id,而非 parent。否则,Django 会尝试将查询结果中名为 parent 的值(可能是字符串或 NULL)直接赋给 MenuEntry.parent 关系属性,从而触发类型校验失败。
✅ 正确做法是:确保 CTE 查询中所有外键字段的列名与 Django 模型中对应的 _id 字段名完全一致。
以下是修正后的完整示例:
from myapp.models import MenuEntry
query = """
WITH RECURSIVE my_menu_tree (id, parent_id, text, menu_id) AS (
-- 种子查询:获取指定菜单下的根级 MenuEntry(parent_id IS NULL)
SELECT id, parent_id, text, menu_id
FROM menu_menuentry
WHERE menu_id IN (
SELECT id FROM menu_menu WHERE name = %s
)
UNION ALL
-- 递归查询:连接子节点
SELECT m.id, m.parent_id, m.text, m.menu_id
FROM menu_menuentry m
INNER JOIN my_menu_tree t ON m.parent_id = t.id
)
SELECT id, parent_id, text, menu_id FROM my_menu_tree;
"""
queryset = MenuEntry.objects.raw(query, ["menu_B"])
# ✅ 现在可以安全访问:
for entry in queryset:
print(f"{entry.text} (parent_id={entry.parent_id})")
# 注意:entry.parent 仍为惰性关系,若需访问父对象需确保 parent_id 存在且已查库(raw() 不自动 prefetch)? 关键修正点总结:
必需(JOIN 条件已隐含),可移除以提升可读性; ⚠️ 注意事项:
通过精准匹配字段名与 Django 的 _id 命名约定,即可让 raw() 安全承载复杂递归逻辑,兼顾性能与可控性。