Django Admin 自定义字段需用 formfield_overrides 轻量替换或 get_form 动态构造表单;列表页跨表字段须用方法+admin_order_field+list_select_related;重写 get_queryset() 时避免预加载干扰 search_fields;批量操作优先用 update() 或 bulk_update() 并加锁。

怎么让 Django Admin 支持自定义字段编辑(比如富文本、JSON 配置)
Django Admin 默认对 TextField 渲染成普通多行输入框,想换 CKEditor 或 JSONEditor 必须绕过默认表单逻辑。直接在 ModelAdmin 里改 formfield_overrides 最轻量,但只适用于简单替换;复杂交互(比如依赖其他字段动态加载选项)得用 get_form 手动构造表单类。
常见错误是把前端组件 JS/CSS 直接塞进 Media 类却忘了加 change_form.html 模板钩子,结果页面没报错但组件不初始化。Django 4.2+ 还要注意 django.contrib.admin.widgets.AdminTextareaWidget 的继承链变了,旧版重写 render() 的方式可能失效。
-
formfield_overrides适合全局统一替换,例如:{models.TextField: {'widget': CKEditorWidget}} - 需要字段级逻辑时,在
get_form()里用forms.ModelForm子类动态设置widget和initial - JS 初始化必须等 DOM 就绪,推荐用
django.contrib.admin.ModelAdmin.Media注入,别用内联<script> - JSON 字段建议用
django-json-widget第三方包,它自动处理None/ 空字符串兼容性,比手写JSONField+ 自定义 widget 更稳
如何在 Admin 列表页(changelist)显示关联模型的字段(比如 User.username)
直接在 list_display 写 user__username 会报 FieldError: Cannot resolve keyword 'user__username' into field —— 因为 list_display 不支持跨表双下划线语法。正确做法是定义一个方法并绑定 admin_order_field,同时在 list_select_related 里声明关联关系,否则每行都触发 N+1 查询。
容易忽略的是排序行为:如果方法返回值经过计算(比如拼接字符串),admin_order_field 只能指定原始字段名,数据库排序仍走关联表字段,不是方法返回值。
立即学习“Python免费学习笔记(深入)”;
- 写方法如
def user_name(self, obj): return obj.user.username if obj.user else '-',然后加user_name.admin_order_field = 'user__username' - 必须配
list_select_related = ('user',),否则性能崩掉;多层关联写('user__profile',) - 方法名不能和模型字段同名,否则 Django 会优先取字段值而跳过方法逻辑
- 空值处理要显式写,
obj.user_id比obj.user更快(避免触发 ORM 实例化)
为什么重写 get_queryset() 后搜索(search_fields)失效或报错
重写 get_queryset() 时如果用了 .select_related() 或 .prefetch_related(),再配合 search_fields 里的跨表字段(如 'author__name'),Django 会尝试在已预加载的 QuerySet 上再做 JOIN,导致 Cannot resolve keyword 或生成冗余 SQL。根本原因是 search_fields 的查询逻辑在 get_queryset() 返回后才执行,但预加载已固化了 JOIN 路径。
更隐蔽的问题是过滤器(list_filter)和搜索共存时,Django 会合并两个 QuerySet,若你重写的 get_queryset() 返回了带 distinct() 的结果,搜索可能漏数据 —— 因为全文检索本身可能产生重复行。
- 搜索字段涉及外键时,
get_queryset()里不要提前select_related,让 Admin 自己处理 JOIN - 真要优化关联查询,用
search_fields = ['title', 'author__name']配合数据库索引,而不是靠预加载 - 如果必须重写
get_queryset()做权限过滤,确保不调用distinct(),除非你清楚搜索字段是否会导致重复 - 调试时打印
request.GET.dict()和最终 SQL(str(qs.query))比看日志更快定位 JOIN 冲突
Admin 操作(actions)里怎么安全地批量修改关联对象(比如给一批订单标记发货)
在 ModelAdmin.actions 方法里直接循环调用 obj.save() 看似简单,但会触发每个对象的完整 Model save 流程(信号、验证、数据库事务),批量 1000 条可能卡死。更糟的是,如果操作涉及外键更新(如 order.status = 'shipped'),没加 select_for_update() 可能导致并发修改丢失。
另一个坑是 request 对象在 action 方法里不可用于获取用户权限上下文 —— 它只传给 action 函数一次,不能直接拿去查 request.user.is_staff 做细粒度校验,得提前在 get_actions() 或 action 方法开头做判断。
- 用
Model.objects.filter(pk__in=ids).update(status='shipped')替代循环 save,跳过信号和验证(注意:这也不会触发post_save) - 需要触发信号?改用
bulk_update(),但必须先 fetch 对象列表,且只支持字段名列表,不能带表达式 - 并发安全必须加锁:
Order.objects.select_for_update().filter(pk__in=ids),记得在事务里执行 - 权限检查别偷懒,action 方法第一行就该有
if not request.user.has_perm('app.can_ship_order'):,否则按钮点了才报 403
真正麻烦的从来不是怎么写代码,而是搞清 Django Admin 在哪一层接管了查询、在哪一层绕过了你的逻辑。比如 search_fields 的 JOIN 是在 ORM 层拼的,但 list_display 方法的执行时机在 Python 层,这两者混着改,出问题连 traceback 都不指向你写的那行。


















