ORM, @property und Celery-Debugging:

„Warum gibt es bei .only() scheinbar kein Feld?“



Wenn man ein System mit hohem Sicherheitsbedarf entwirft, speichert man häufig API‑Keys oder Secret‑Werte in verschlüsselter Form in der Datenbank und gibt sie beim Zugriff nur über ein @property als entschlüsselten Wert frei.

Zum Beispiel sieht das ungefähr so aus.

  • _api_key → tatsächliche DB‑Spalte (verschlüsselter Wert)
  • api_key@property, das den Wert entschlüsselt zurückgibt
  • api_key.setter → verschlüsselt den Wert beim Speichern

Dieses Muster ist sauber im Code und natürlich sicher. Doch beim Einsatz mit dem Django ORM gibt es eine häufige Falle.

„Ich benutze api_key im Modell, aber die ORM‑Abfrage sagt, das Feld existiert nicht?“

In diesem Beitrag erkläre ich, wie diese Falle entsteht, warum das Debugging besonders schwierig ist und wie man sie vermeidet.


1. Problemstellung: „Ich habe den Wert, aber das ORM sagt, das Feld existiert nicht?“

Während ich ein Modell mit verschlüsseltem Feld und @property betreibe, führte das ORM plötzlich folgenden Code aus.

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

oder:

Model.objects.values("api_key")

oder:

Model.objects.filter(api_key__isnull=False)

Das Ergebnis war immer gleich.

FieldDoesNotExist: Model has no field named 'api_key'

Als Entwickler wollte ich sagen:

„Nein, das Feld existiert. Ich benutze obj.api_key täglich.“

Das Modell sah tatsächlich so aus (vereinfachtes Beispiel).

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)
  • Auf Instanzebene funktioniert obj.api_key einwandfrei.
  • Speichern funktioniert ebenfalls mit obj.api_key = "plain-text".

Doch auf ORM‑Ebene behauptet es, das Feld nicht zu kennen. Das mag seltsam erscheinen, ist aber für das ORM völlig normal.


2. Ursache: Das ORM erkennt den „Feldnamen“ nicht, weil es sich um eine Property handelt



Der Kern ist einfach.

api_key, das mit @property erstellt wurde, ist kein echtes Feld.

Für das Django ORM ist ein Feld streng genommen eine models.Field-Unterklasse. Das heißt, alles, was in Model._meta.fields steht, ist ein echtes Feld.

In meinem Beispiel kennt das ORM nur:

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

Hier gilt:

  • Modell‑Feldname_api_key
  • DB‑Spaltenname"api_key"
  • @property api_key → Dem ORM völlig unbekannt

Zusammengefasst:

Was ich täglich benutze (obj.api_key) Was das ORM kennt (Model._meta)
api_key (Property) _api_key (echtes Feld)

Deshalb führt

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

zu einem Fehler, weil das ORM das Feld nicht findet.

Das ORM arbeitet immer nach dem Feldnamen.

  • only("_api_key")
  • values("_api_key")
  • filter(_api_key__isnull=False)
  • only("api_key")
  • filter(api_key=...)

Kurz gesagt: Verwechslung zwischen dem Property‑Namen und dem Feldnamen.


3. Warum ist das in Celery noch schlimmer?

Bis hierhin sieht es wie ein leicht zu behebendes Problem aus. Der eigentliche Knackpunkt ist, dass der Code in einer Celery‑Aufgabe lief.

Angenommen, die Aufgabe sieht so aus.

@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)

Der eigentliche Fehler ist:

  • only("api_key") wirft FieldDoesNotExist.
  • Diese Ausnahme fällt in eine Kategorie, die Celery als temporären Fehler interpretiert.
  • Celery ruft self.retry() auf und versucht die Aufgabe erneut.

Das Ergebnis: Der Fehler ist versteckt hinter dem Retry‑Mechanismus und sieht aus wie ein Netzwerk‑Problem.

Anzeichen:

  • Celery‑Aufgabe bleibt in Retry‑Status.
  • Externe API wird nicht aufgerufen.
  • Ohne genaue Log‑Analyse könnte man denken, die API sei ausgefallen.

Das Debugging wird dadurch deutlich schwieriger.


4. Lösung: Immer den echten Feldnamen an das ORM übergeben

Die Lösung ist simpel.

Properties nur auf Instanzebene nutzen, im ORM immer den echten Feldnamen verwenden.

# Falsch (FieldDoesNotExist)
obj = Model.objects.only("api_key").get(...)
# Richtig
obj = Model.objects.only("_api_key").get(...)
real_key = obj.api_key  # Hier wird die Property benutzt

Dasselbe gilt für Filter.

# Falsch
Model.objects.filter(api_key__isnull=False)
Model.objects.filter(api_key="plain-text")
# Richtig
Model.objects.filter(_api_key__isnull=False)
Model.objects.filter(_api_key=encrypt("plain-text"))

Um noch sicherer zu sein, kann man im Team eine Regel einführen, dass keine Property‑Namen in ORM‑Aufrufen verwendet werden.

Beispielregeln:

  • Felder, die mit _ beginnen, nur im ORM benutzen.
  • Exponierte Property‑Namen niemals in .values(), .only() oder .filter().
  • Code‑Reviews prüfen explizit auf diese Muster.

5. Tipp: Eigene Feldklasse statt Property

In größeren Projekten kann es sinnvoll sein, statt einer Property eine eigene Feldklasse zu erstellen.

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)

Dann ist api_key ein echtes Feld, und man kann problemlos

  • .only("api_key")
  • .values("api_key")
  • .filter(api_key__isnull=False)

Alle funktionieren.

Vorteile:

  • ORM erkennt das Feld.
  • Code bleibt sauber.

Nachteile:

  • Implementierung und Tests der Feldklasse.
  • Migrations‑ und Kompatibilitätsfragen.

Je nach Teamgröße und Komplexität kann man zwischen Property + Regel oder Custom Field wählen.


6. Erkenntnis: Einfach, aber leicht zu übersehen

Die Lehre aus dieser Erfahrung ist einfach, aber leicht zu übersehen.

  • Wenn man nur obj.api_key sieht, verliert man das Gefühl, dass es kein echtes Feld ist.
  • Ein einziger Fehler in einer langen ORM‑Zeile kann die Ursache sein.
  • In asynchronen Umgebungen wie Celery verschwindet der Fehler hinter Retries.

Um solche Zeitverschwendung zu vermeiden, behalte die folgenden Punkte im Kopf.


7. Zusammenfassung

1) Das ORM versteht nur Modell‑Feldnamen.

  • Model._meta.fields ist die Basis.
  • @property gehört nicht dazu.

2) @property kann nicht in ORM‑Abfragen verwendet werden.

  • .only("api_key"), .filter(api_key=...) sind falsch.
  • Stattdessen immer den echten Feldnamen (_api_key) benutzen.

3) Verschlüsselungs‑/Entschlüsselungs‑Pattern erfordern besondere Vorsicht.

  • Ähnliche Namen können verwirren.
  • Reviews sollten diese Punkte prüfen.

4) In Celery kann der Fehler als Netzwerk‑Problem getarnt werden.

  • Retry‑Mechanismus kann den Fehler verbergen.
  • Log‑Analyse und Fehler‑Handling sind entscheidend.

Sichere Felder zu verbergen ist sinnvoll, aber man muss die Interaktion mit dem ORM und asynchronen Workern berücksichtigen, um ähnliche Probleme zu vermeiden.

image