ORM, @property et débogage Celery
“Pourquoi .only() dit qu’un champ qui existe n’existe pas ?”
Lors de la conception d’un système où la sécurité est primordiale, on stocke souvent les clés API ou secrets chiffrés dans le modèle, puis on expose uniquement la valeur déchiffrée via un @property.
Par exemple, la structure est généralement la suivante :
_api_key→ colonne réelle de la base de données (valeur chiffrée)api_key→@propertyqui déchiffre et renvoie la valeurapi_key.setter→ chiffre automatiquement la valeur lors de la sauvegarde
Cette approche rend le code propre et la sécurité naturelle. Mais lorsqu’on l’utilise avec l’ORM (Django ORM), un piège simple se glisse.
“Le champ
api_keyque j’utilise dans le modèle n’existe pas dans l’ORM ?”
Cet article explique comment ce piège se produit, pourquoi le débogage devient si difficile, et comment l’éviter.
1. Situation problématique : le champ existe, mais l’ORM dit qu’il n’existe pas
En exploitant un modèle avec un champ chiffré encapsulé dans une @property, j’ai exécuté le code suivant :
Model.objects.only("api_key").first()
ou encore :
Model.objects.values("api_key")
ou encore :
Model.objects.filter(api_key__isnull=False)
Le résultat était toujours le même :
FieldDoesNotExist: Model has no field named 'api_key'
En tant que développeur, je pensais :
“Il existe. Je l’utilise quotidiennement avec
obj.api_key.”
Le modèle était en fait :
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)
- L’instance fonctionne avec
obj.api_key - La sauvegarde fonctionne avec
obj.api_key = "plain-text"
Mais l’ORM refuse de reconnaître le champ au niveau de la requête.
2. Pourquoi l’ORM ne voit pas la @property
La raison est simple :
La
@propertyn’est pas un vrai champ.
Dans Django, un champ est une sous-classe de models.Field. L’ORM ne connaît que les éléments de Model._meta.fields.
Dans l’exemple ci‑dessus, l’ORM ne sait que :
_api_key = models.CharField(db_column="api_key", max_length=255, null=True)
- Nom du champ :
_api_key - Nom de la colonne :
"api_key" @property api_key: l’ORM ne le connaît pas.
En résumé :
Nom que j’utilise (obj.api_key) |
Nom que l’ORM connaît (Model._meta) |
|---|---|
api_key (propriété) |
_api_key (champ) |
Donc, lorsqu’on écrit :
Model.objects.only("api_key")
Model.objects.values("api_key")
Model.objects.filter(api_key__isnull=False)
l’ORM considère qu’il n’existe pas de champ api_key.
L’ORM ne traite que les noms de champs.
- ✅
only("_api_key") - ✅
values("_api_key") - ✅
filter(_api_key__isnull=False) - ❌
only("api_key") - ❌
filter(api_key=...)
En bref : c’est un mélange entre le nom de la propriété Python et le nom du champ réel.
3. Pourquoi c’est encore pire dans Celery
Ce problème devient critique lorsqu’il se produit dans une tâche Celery.
Prenons un exemple typique :
@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)
Le vrai problème :
only("api_key")lèveFieldDoesNotExist- Cette exception est capturée par un bloc
exceptgénérique ou par la logique de retry de Celery - Celery interprète l’erreur comme un problème temporaire et la relance
Résultat : la tâche se retrouve en boucle de retry, aucune requête vers l’API externe n’est envoyée, et le problème reste caché derrière le mécanisme de retry.
Le débogage devient alors beaucoup plus difficile, car l’erreur ne se produit pas immédiatement mais est absorbée par Celery.
4. Solution : toujours passer le nom du champ réel à l’ORM
La règle est simple :
Utilisez uniquement le nom du champ réel dans les requêtes ORM.
# Mauvais : FieldDoesNotExist
obj = Model.objects.only("api_key").get(...)
# Correct :
obj = Model.objects.only("_api_key").get(...)
real_key = obj.api_key # accès via la propriété
Même pour les filtres :
# Mauvais :
Model.objects.filter(api_key__isnull=False)
Model.objects.filter(api_key="plain-text")
# Correct :
Model.objects.filter(_api_key__isnull=False)
Model.objects.filter(_api_key=encrypt("plain-text"))
Pour éviter ce piège, il est judicieux d’instaurer une règle d’équipe :
- Les champs commençant par
_sont réservés à l’ORM. - Les propriétés exposées publiquement ne doivent jamais être utilisées dans les requêtes ORM.
- Revues de code : vérifier que
.values("..."),.only("..."),.filter(...)n’utilisent pas de noms de propriétés.
5. Astuce supplémentaire : créer un champ personnalisé
Dans les projets de grande envergure, on peut préférer créer un champ personnalisé au lieu d’une @property.
Par exemple :
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)
Avec ce champ :
- L’ORM reconnaît
api_keycomme un vrai champ. - Vous pouvez utiliser
only("api_key"),values("api_key"),filter(api_key__isnull=False)sans problème.
Inconvénients :
- Implémentation et tests supplémentaires.
- Gestion des migrations et compatibilité.
Choisissez la solution qui convient le mieux à votre équipe.
6. Leçon apprise : un détail qui peut coûter cher
Cette expérience montre que même un détail apparemment mineur peut devenir un gouffre de bugs, surtout dans un environnement asynchrone comme Celery.
Points clés :
- Ne confondez pas le nom de la propriété Python avec le nom du champ réel.
- Vérifiez toujours vos requêtes ORM pour vous assurer qu’elles utilisent le bon nom.
- En cas d’erreur
FieldDoesNotExist, pensez à la différence entre le champ et la propriété. - Dans Celery, les exceptions peuvent être masquées par le mécanisme de retry, rendant le débogage plus ardu.
7. Résumé
1) L’ORM ne comprend que les noms de champs réels.
Model._meta.fieldsdéfinit ce que l’ORM connaît.- Les
@propertyne sont pas incluses.
2) Ne jamais utiliser une propriété dans une requête ORM.
- Utilisez toujours le nom du champ réel (
_api_key).
3) Soyez vigilant avec les champs chiffrés.
- Les noms proches peuvent prêter à confusion.
- Revues de code sont essentielles.
4) En Celery, les bugs peuvent se masquer derrière le retry.
- Vérifiez les logs et la gestion des exceptions.
En adoptant ces bonnes pratiques, vous éviterez de perdre des journées à traquer des bugs qui se cachent derrière des erreurs de champ.

Aucun commentaire.