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@property die de waarde ontsleutelt
  • api_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_key in 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_key prima.
  • 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 @property is 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:

  • FieldDoesNotExist bij only("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_key als een echt veld.
  • Je kunt nog steeds obj.api_key gebruiken 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 FieldDoesNotExist fout 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.fields bepaalt wat er beschikbaar is.
  • @property valt 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.

image