ORM、@property 与 Celery 调试:

“为什么 .only() 里说不存在该字段?”



在设计安全性要求高的系统时,常会把 API Key 或 Secret 以加密形式存储在模型中,并通过 @property 在访问时只暴露解密后的值

例如,结构大致如下:

  • _api_key → 实际 DB 列(存储加密值)
  • api_key → 通过 @property 解密后返回
  • api_key.setter → 保存时自动加密

这种模式既保持了代码整洁,又符合安全要求。

但在与 ORM(以 Django ORM 为例)一起使用时,往往会不经意间踩到一个陷阱。

“我在模型里一直用 api_key,为什么 ORM 说字段不存在?”

本文记录了该陷阱的产生原因、调试难点以及如何避免。


1. 问题情境:

“我明明有值,ORM 说没有字段?”

在使用加密字段的模型时,某天 ORM 执行了以下代码:

Model.objects.only("api_key").first()

或:

Model.objects.values("api_key")

或:

Model.objects.filter(api_key__isnull=False)

结果总是相同:

FieldDoesNotExist: Model has no field named 'api_key'

开发者自然会想:

“我每天都用 obj.api_key,它是存在的。”

模型实际定义(简化示例)如下:

class Model(models.Model):
    _api_key = models.CharField(db_column="api_key", max_length=255, null=True)

    @property
    def api_key(self):
        if not self._api_key:
            return None
        return decrypt(self._api_key)

    @api_key.setter
    def api_key(self, value):
        self._api_key = encrypt(value)
  • 实例层:obj.api_key 正常工作
  • 保存层:obj.api_key = "plain-text" 也正常

但 ORM 层却认为字段不存在。为什么?


2. 原因:ORM 识别的“字段名”不是属性名



核心很简单:

@property 创建的 api_key 并不是真正的字段。

Django ORM 所说的“字段”,指的是继承自 models.Field 的子类。 也就是说,Model._meta.fields 中列出的都是实际字段。

在上述例子中,ORM 只知道:

_api_key = models.CharField(db_column="api_key", max_length=255, null=True)

这里:

  • 模型字段名_api_key
  • 数据库列名"api_key"
  • @property api_key → ORM 完全不知道

总结表格:

我们日常使用的名字 (obj.api_key) ORM 认识的名字 (Model._meta)
api_key (属性) _api_key (字段名)

因此,ORM 里写:

Model.objects.only("api_key")
Model.objects.values("api_key")
Model.objects.filter(api_key__isnull=False)

会导致 ORM 认为“没有该字段”。

正确写法应使用字段名:

Model.objects.only("_api_key")
Model.objects.values("_api_key")
Model.objects.filter(_api_key__isnull=False)

3. Celery 内部为何更致命?

这类错误在 Django 视图里看起来只是“稍微麻烦”,但如果出现在 Celery 任务内部,后果更严重。

假设任务如下:

@shared_task(bind=True, max_retries=3)
def send_something(self, model_id):
    try:
        obj = Model.objects.only("api_key").get(id=model_id)
        call_external_api(obj.api_key)
    except SomeRecoverableError as exc:
        raise self.retry(exc=exc)

真正的问题是:

  • only("api_key") 抛出 FieldDoesNotExist
  • 该异常被某个上层捕获,或被 Celery 误认为“可恢复错误”
  • Celery 于是执行 self.retry(),导致任务不断重试

结果:

  • Celery 任务一直在重试
  • 外部 API 根本没有收到请求
  • 日志里只看到“网络问题”或“外部服务器无响应”,很容易误判

这就是为什么调试难度会“爆炸性”提升的原因。


4. 解决方案:始终使用字段名

结论很简单:

属性只在实例层使用,ORM 查询时始终使用真正的字段名。

示例:

# 错误示例(会抛 FieldDoesNotExist)
obj = Model.objects.only("api_key").get(...)
# 正确示例
obj = Model.objects.only("_api_key").get(...)
real_key = obj.api_key  # 这里再使用属性

过滤时同理:

# 错误
Model.objects.filter(api_key__isnull=False)
Model.objects.filter(api_key="plain-text")
# 正确
Model.objects.filter(_api_key__isnull=False)
Model.objects.filter(_api_key=encrypt("plain-text"))

如果想更安全,可以在团队层面制定规则:

  • “以 _ 开头的字段仅在 ORM 中使用”
  • “对外暴露的属性名不用于 ORM 查询”
  • 代码审查时检查 .values("...").only("...").filter(...) 是否使用了属性名

5. 额外技巧:使用自定义字段隐藏加密逻辑

在大型项目中,可以考虑用自定义字段代替 @property

示例:

class EncryptedCharField(models.CharField):
    def get_prep_value(self, value):
        if value is None:
            return None
        return encrypt(value)

    def from_db_value(self, value, expression, connection):
        if value is None:
            return None
        return decrypt(value)

class Model(models.Model):
    api_key = EncryptedCharField(max_length=255, db_column="api_key", null=True)

这样:

  • ORM 认为 api_key 是真正字段
  • 代码层仍可直接 obj.api_key 使用
  • .only("api_key").values("api_key").filter(api_key__isnull=False) 都能正常工作

缺点:

  • 需要实现并测试自定义字段
  • 迁移和兼容性需注意

根据团队规模和需求,选择“简单的 @property + 规则”或“自定义字段”即可。


6. 经验总结:细节决定成败

这次经历的教训很直白:

  • @property 并不属于 ORM 字段
  • 在 Celery 等异步环境中,错误可能被隐藏在重试机制里
  • 代码审查时要特别留意 ORM 查询是否使用了属性名

记住以下四点即可大幅减少类似问题:

  1. ORM 只识别模型字段名
  2. @property 不能用于 ORM 查询
  3. 加密/解密模式下,字段名与属性名易混淆
  4. Celery 内部的错误可能被误认为网络问题

7. 归纳

1) ORM 只理解“模型字段名”

  • Model._meta.fields 为唯一来源
  • @property 不在其中

2) @property 不能用于 ORM 查询

  • .only("api_key").filter(api_key=...) 均错误
  • 必须使用 _api_key

3) 加密/解密模式需更谨慎

  • 字段名与属性名相似易混
  • 代码审查时要检查

4) Celery 内部更危险

  • 异常被重试机制吞掉
  • 看似网络问题,实则字段错误

通过合理的命名约定和代码审查,可避免类似问题耗费大量时间。

image