ORM, @property и отладка Celery:
«Почему в .only() явно существующее поле отсутствует?»
При проектировании систем, где безопасность критична, часто сохраняют значения API‑ключей или секретов в модели в зашифрованном виде, а при доступе через @property раскрывают только дешифрованные значения.
Например, типичная структура выглядит так.
_api_key→ реальный столбец БД (зашифрованное значение)api_key→@property, возвращающий расшифрованное значениеapi_key.setter→ при сохранении автоматически шифрует
Эта схема чистая по коду и естественная с точки зрения безопасности. Однако при работе с ORM (на примере Django ORM) легко попасть в ловушку.
«В модели я использую
api_key, но в запросе ORM говорит, что поля нет?»
В этой статье описывается, как возникла эта проблема, почему отладка была особенно трудной и как её избежать.
1. Ситуация: «Есть значение, но ORM говорит, что поля нет»
При эксплуатации модели с зашифрованным полем, обернутым в @property, однажды 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 «поле отсутствует». Это не странно с точки зрения ORM.
2. Причина: ORM «видит» только имена полей, а не свойства
Ключевой момент прост: @property не является настоящим полем.
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 ничего не знает
Итого:
| Что мы используем в коде | Что знает ORM |
|---|---|
api_key (property) |
_api_key |
Поэтому запросы вида:
Model.objects.only("api_key")
Model.objects.values("api_key")
Model.objects.filter(api_key__isnull=False)
вызывают ошибку, потому что ORM ищет поле api_key, которого нет.
Правильные варианты:
only("_api_key")values("_api_key")filter(_api_key__isnull=False)
3. Почему это критично в Celery?
Проблема становится более серьёзной, когда код находится внутри 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)
Здесь FieldDoesNotExist выбрасывается, но Celery перехватывает исключение как «временную ошибку» и автоматически повторяет задачу. В итоге:
- Задача постоянно переходит в режим
retry - Внешний API никогда не получает запросов
- Логи могут показать «сеть не отвечает», хотя проблема в коде
Отладка становится сложной, потому что исключение «прячется» внутри механизма повторных попыток.
4. Решение: всегда использовать реальные имена полей
Ключевой принцип: свойства (@property) используют только в коде, а в запросах ORM всегда передаём реальные имена полей.
# Неправильно
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__isnull=False)
Для большей безопасности можно в проекте закрепить правило:
- Поля, начинающиеся с
_, используются только в 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)
Теперь api_key является настоящим полем ORM, но в коде выглядит как обычное строковое поле.
Преимущества:
- ORM понимает
api_key - Можно использовать
only("api_key"),values("api_key"),filter(api_key__isnull=False)без ошибок
Недостатки:
- Нужно реализовать и протестировать кастомное поле
- Миграции и совместимость
Выбор зависит от масштаба и команды.
6. Выводы
- ORM понимает только имена полей, а не свойства
- В Celery ошибки
FieldDoesNotExistмогут маскироваться как сетевые проблемы - Всегда используйте реальные имена полей в запросах
- Рассмотрите кастомные поля для упрощения работы
Эти простые правила помогут избежать потери времени на отладку и сделают ваш код более надёжным.

Комментариев нет.