Django 模型创建时无限等待问题的根源与修复方案

本文揭示 django 模型实例化或序列化

卡住(无报错、无响应)的典型原因:`default=` 函数中误用 `objects.create()` 导致数据库死锁或无限循环,重点解析 `generate_unique_id()` 的危险实现及安全替代方案。

在 Django(尤其是配合 Django REST Framework)开发中,模型创建“静默卡住”——如 shell 中执行 AllowedUser(...) 后光标悬停、DRF 视图中 serializer.save() 之后无日志无响应、进程不崩溃也不继续——往往并非数据库连接超时或硬件问题,而是 default 字段函数内部触发了未完成的数据库操作,形成隐式递归或事务阻塞。

你提供的 generate_unique_id() 函数看似合理,但原始错误版本的关键缺陷在于:

# ❌ 危险写法(原文未贴出,但答案已指出):
def generate_unique_id():
    while True:
        new_id = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8)).upper()
        # 错误:使用 create() 而非 filter(),且未提供必需字段 → 触发完整模型验证与保存
        obj = AllowedUser.objects.create(id=new_id)  # ← 这里会调用 __init__ + save() → 再次触发 generate_unique_id()!
        return new_id

该逻辑造成无限递归调用链
AllowedUser(...) → 初始化时调用 default=generate_unique_id → generate_unique_id 内部调用 objects.create() → create() 实例化新 AllowedUser → 再次触发 default=generate_unique_id → ……
SQLite3 在此场景下因缺乏并发锁提示,常表现为“冻结”(实际是深度递归或死锁等待),而非抛出 RecursionError 或 IntegrityError。

✅ 正确做法是:default 函数必须是纯查询、无副作用、不触发模型保存。你修正后的版本使用 filter().exists() 是安全的:

import random
import string
from django.db import models

def generate_unique_id():
    max_attempts = 100
    characters = string.ascii_letters + string.digits
    for _ in range(max_attempts):
        new_id = ''.join(random.choice(characters) for _ in range(8)).upper()
        # ✅ 安全:仅查询,不创建、不保存、不触发信号或验证
        if not AllowedUser.objects.filter(id=new_id).exists():
            return new_id
    raise RuntimeError("Failed to generate unique ID after {} attempts".format(max_attempts))
? 关键原则:default(或 default_callable)函数中禁止调用 Model.objects.create()、save()、full_clean() 等任何可能引发模型生命周期钩子(如 pre_save、post_save)或再次触发 default 计算的操作。

此外,为提升健壮性,建议对 id 字段补充数据库层唯一性保障,并优化默认值逻辑:

class AllowedUser(models.Model):
    PLACE_CHOICES = [
        (1, 'Loc1'),
        (2, 'Loc2'),
        (3, 'Loc3'),
        (4, 'Loc4'),
        (5, 'Loc5'),
    ]

    id = models.CharField(
        primary_key=True,
        max_length=8,
        unique=True,
        default=generate_unique_id,
        help_text="Auto-generated 8-character alphanumeric ID"
    )
    name = models.CharField(max_length=60)
    place = models.IntegerField(choices=PLACE_CHOICES)
    current_version = models.CharField(max_length=8, default="0.0.1")
    last_updated = models.DateTimeField(default=lambda: datetime(1970, 1, 1, 0, 0, 0))

    def __str__(self):
        return f"{self.id} - {self.name}"

⚠️ 注意事项:

  • datetime.datetime(1970,1,1,0,0,0) 应改为 datetime(1970, 1, 1, 0, 0, 0)(避免 datetime.datetime 重复命名);
  • 生产环境建议改用 uuid.uuid4().hex[:8].upper() 替代随机生成,显著降低碰撞概率;
  • 若需强一致性,可结合数据库 UNIQUE 约束 + try/except IntegrityError 重试机制,而非纯应用层检查。

总结:Django 模型“冻结”多源于 default 函数的隐式副作用。坚守「只读查询、无状态、有限重试」三原则,即可彻底规避此类静默故障。