ORM, @property en Celery debugging:
“Waarom zegt .only() dat een veld niet bestaat?”
Bij het ontwerpen van een systeem waar veiligheid cruciaal is, slaan we vaak API‑sleutels of geheimen in een versleutelde vorm op in het model, en tonen we alleen de gedecodeerde waarde via een @property.
Een typische structuur ziet er zo uit:
_api_key→ echte DB‑kolom (versleutelde waarde)api_key→@propertydie de waarde ontsleuteltapi_key.setter→ versleutelt de waarde bij opslaan
Dit is een nette en veilige aanpak. Maar wanneer je het combineert met een ORM (bijv. Django ORM), valt er een valkuil die vaak over het hoofd wordt gezien.
“Ik gebruik
api_keyin het model, maar de ORM zegt dat het veld niet bestaat?”
In dit artikel leg ik uit hoe die valkuil ontstaat, waarom debugging bijzonder lastig is, en hoe je het kunt voorkomen.
1. Probleem: “Er is een waarde, maar de ORM zegt dat het veld niet bestaat?”
Tijdens het draaien van een model met versleutelde velden, werd de volgende query uitgevoerd:
Model.objects.only("api_key").first()
of
Model.objects.values("api_key")
of
Model.objects.filter(api_key__isnull=False)
De uitkomst was altijd hetzelfde:
FieldDoesNotExist: Model has no field named 'api_key'
Een ontwikkelaar zou denken: “Maar het bestaat, ik gebruik het dagelijks via obj.api_key.”
Het model was als volgt (verkort voorbeeld):
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)
- Op instantie‑niveau werkt
obj.api_keyprima. - Opslaan via
obj.api_key = "plain-text"werkt ook.
Maar op ORM‑niveau wordt er gefrustreerd “veld bestaat niet”.
2. Oorzaak: De ORM herkent de “veldnaam” niet als een property
Het probleem is simpel:
Een
@propertyis geen echt veld.
Django ORM noemt een veld een subklasse van models.Field. De lijst van zulke velden staat in Model._meta.fields.
In het voorbeeld weet de ORM alleen van:
_api_key = models.CharField(db_column="api_key", max_length=255, null=True)
Hier is:
- Model‑veldnaam →
_api_key - DB‑kolomnaam →
"api_key" - @property api_key → onbekend voor de ORM
Samengevat:
| Gebruikte naam in code | Naam die de ORM kent |
|---|---|
api_key (property) |
_api_key (veldnaam) |
Daarom werkt:
Model.objects.only("_api_key")
Model.objects.values("_api_key")
Model.objects.filter(_api_key__isnull=False)
Maar niet:
Model.objects.only("api_key")
Model.objects.filter(api_key=...)
Het is dus een verwarring tussen de property‑naam op Python‑niveau en de veldnaam op ORM‑niveau.
3. Waarom is dit nog erger in Celery?
Het probleem lijkt simpel, maar het wordt veel erger wanneer de code in een Celery‑taak draait.
Stel je voor:
@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)
De echte fout is:
FieldDoesNotExistbijonly("api_key")- Deze fout valt in een except‑blok dat wordt herkend als een recoverable error
- Celery herkent het als een tijdelijke fout en retryt de taak
Resultaat:
- De taak blijft retryen
- De externe API wordt nooit aangeroepen
- Zonder gedetailleerde logs lijkt het alsof er een netwerkprobleem is
Het debuggen wordt daardoor veel moeilijker, omdat de fout zich verbergt achter de retry‑mechaniek.
4. Oplossing: Gebruik altijd de echte veldnaam in ORM‑queries
De oplossing is eenvoudig:
Gebruik alleen de echte veldnaam in ORM‑queries.
# Foutief
obj = Model.objects.only("api_key").get(...)
# Correct
obj = Model.objects.only("_api_key").get(...)
real_key = obj.api_key # property wordt nu gebruikt
Ook bij filtering:
# Foutief
Model.objects.filter(api_key__isnull=False)
# Correct
Model.objects.filter(_api_key__isnull=False)
Model.objects.filter(_api_key=encrypt("plain-text"))
Een goede praktijk is om een team‑regel op te stellen, bijvoorbeeld:
- “Velden die beginnen met
_worden alleen in ORM‑queries gebruikt.” - “Properties die naar buiten worden blootgesteld, mogen niet in ORM‑queries voorkomen.”
- “Review
.values(),.only(),.filter()op property‑namen.”
5. Extra tip: Gebruik een custom veld voor versleuteling
In grotere projecten kun je overwegen een custom model‑veld te maken in plaats van een 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)
Voordelen:
- De ORM ziet
api_keyals een echt veld. - Je kunt nog steeds
obj.api_keygebruiken alsof het een gewone string is. - Alle ORM‑queries werken met
api_key.
Nadelen:
- Extra werk voor het implementeren en testen van het custom veld.
- Migraties en compatibiliteit moeten worden overwogen.
Kies afhankelijk van de schaal en team‑samenstelling.
6. Wat heb ik geleerd?
- De property‑naam en de veldnaam zijn niet hetzelfde.
- In Celery kan een
FieldDoesNotExistfout zich verbergen achter retry‑logica. - Een eenvoudige team‑regel of een custom veld kan veel tijd besparen.
7. Samenvatting
1) De ORM begrijpt alleen model‑veldnamen.
Model._meta.fieldsbepaalt wat er beschikbaar is.@propertyvalt buiten deze lijst.
2) Gebruik geen property‑namen in ORM‑queries.
.only("_api_key"),.filter(_api_key=...)zijn correct.
3) Wees extra voorzichtig met versleutelde velden.
- Duidelijke naamgeving en code‑reviews helpen.
4) In Celery kan een simpele fout zich verbergen achter retry‑mechanismen.
- Controleer exception‑handling en logging.
Door deze punten in acht te nemen, kun je soortgelijke problemen voorkomen en je codebase veiliger en onderhoudbaarder maken.

댓글이 없습니다.