ORM, @property y depuración de Celery:
“¿Por qué .only() dice que el campo no existe cuando claramente sí lo hace?”
Al diseñar sistemas donde la seguridad es crucial, a menudo guardamos valores de API Key o Secret en el modelo en forma cifrada y, al acceder a ellos, exponemos solo el valor descifrado mediante un @property.
Por ejemplo, la estructura suele ser así:
_api_key→ columna real de la BD (valor cifrado)api_key→@propertyque devuelve el valor descifradoapi_key.setter→ cifra automáticamente al guardar
Este patrón mantiene el código limpio y la seguridad natural. Sin embargo, al usarlo con el ORM (Django ORM), hay una trampa que suele pasar desapercibida.
“El modelo usa
api_keysin problemas, pero el ORM dice que el campo no existe”
Este artículo explica cómo surge esa trampa, por qué la depuración es tan difícil y cómo evitarla.
1. Situación del problema: “El valor está ahí, pero el ORM dice que no”
Mientras operaba con un modelo que cifraba el campo con @property, un día el ORM ejecutó el siguiente código:
Model.objects.only("api_key").first()
o bien:
Model.objects.values("api_key")
o:
Model.objects.filter(api_key__isnull=False)
El resultado siempre fue:
FieldDoesNotExist: Model has no field named 'api_key'
Para el desarrollador, la respuesta era: “Claro que sí, lo uso todos los días con obj.api_key”.
El modelo se veía así (simplificado):
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)
- En la instancia funciona con
obj.api_key - Al guardar también funciona con
obj.api_key = "plain-text"
Pero a nivel ORM, el mensaje de error es correcto: el ORM no reconoce api_key como un campo.
2. Causa: el ORM solo reconoce nombres de campos reales, no propiedades
El punto clave es simple:
api_keycreado con@propertyno es un campo real.
El ORM llama a un campo cuando se trata de una subclase de models.Field. En otras palabras, todo lo que aparece en Model._meta.fields es lo que el ORM entiende.
En el ejemplo, el ORM solo conoce:
_api_key = models.CharField(db_column="api_key", max_length=255, null=True)
- Nombre del campo →
_api_key - Nombre de la columna en la BD →
"api_key" @property api_key→ el ORM no lo ve
Resumen en tabla:
| Nombre que usamos en el código | Nombre que el ORM conoce |
|---|---|
api_key (propiedad) |
_api_key (campo) |
Por eso, cuando escribes:
Model.objects.only("api_key")
Model.objects.values("api_key")
Model.objects.filter(api_key__isnull=False)
el ORM responde que el campo no existe.
El ORM siempre opera sobre nombres de campo.
- ✅
only("_api_key") - ✅
values("_api_key") - ✅
filter(_api_key__isnull=False) - ❌
only("api_key") - ❌
filter(api_key=...)
En resumen, la confusión surge al mezclar el nombre de la propiedad con el nombre real del campo.
3. ¿Por qué es más crítico dentro de Celery?
Hasta aquí parece un error “fastidioso pero solucionable”. El problema real es que el código estaba dentro de una tarea de Celery.
Supongamos algo así:
@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)
El verdadero problema es:
only("api_key")lanzaFieldDoesNotExist- Ese error entra en la lógica de reintentos de Celery
- Celery lo interpreta como un “error temporal” y vuelve a intentar
Así, el bug real se oculta detrás de un reintento que parece un problema de red.
Los síntomas típicos:
- La tarea de Celery sigue reintentando
- La API externa nunca recibe la solicitud
- Si no revisas los logs, podrías pensar que la API está lenta o que hay un problema de red
La depuración se vuelve mucho más difícil porque el error no se lanza directamente al stack trace, sino que se “oculta” dentro del mecanismo de reintentos.
4. Solución: usar siempre el nombre real del campo en el ORM
La conclusión es simple:
Usa el nombre real del campo en el ORM; la propiedad solo se usa a nivel de instancia.
Ejemplo correcto:
# Incorrecto (causa FieldDoesNotExist)
obj = Model.objects.only("api_key").get(...)
# Correcto
obj = Model.objects.only("_api_key").get(...)
real_key = obj.api_key # aquí ya usamos la propiedad
Lo mismo aplica al filtrado:
# Incorrecto
Model.objects.filter(api_key__isnull=False)
Model.objects.filter(api_key="plain-text")
# Correcto
Model.objects.filter(_api_key__isnull=False)
Model.objects.filter(_api_key=encrypt("plain-text"))
Para mayor seguridad, puedes establecer reglas de codificación en tu equipo:
- “Los campos que comienzan con
_solo se usan en el ORM” - “Los nombres de propiedades expuestas nunca se usan en consultas ORM”
- Revisar
.values("..."),.only("..."),.filter(...)para asegurarse de que no haya nombres de propiedades.
5. Consejo adicional: usar un campo personalizado en lugar de @property
En proyectos grandes, podrías considerar crear un campo personalizado en lugar de una propiedad.
Por ejemplo, un EncryptedCharField que maneje cifrado/descifrado internamente:
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)
Con esto:
- El ORM reconoce
api_keycomo un campo real - Puedes usar
only("api_key"),values("api_key"),filter(api_key__isnull=False)sin problemas - El código sigue siendo limpio:
obj.api_keydevuelve el valor descifrado
Desventajas:
- Necesitas implementar y probar el campo personalizado
- Consideraciones de migraciones y compatibilidad
Elige la opción que mejor se adapte a tu equipo y proyecto.
6. Lección aprendida: lo simple puede ser fácil de pasar por alto
La moraleja es sencilla pero importante:
- Los nombres de las propiedades pueden confundir al ORM
- En entornos asíncronos como Celery, los errores se ocultan detrás de reintentos
- Revisar cuidadosamente las consultas ORM y establecer reglas claras ayuda a evitar este tipo de bugs
7. Resumen
1) El ORM solo entiende nombres de campos reales
Model._meta.fieldses la referencia@propertyno está incluido
2) No usar @property en consultas ORM
only("api_key"),filter(api_key=...)son incorrectos- Usa siempre el nombre real, por ejemplo
_api_key
3) La seguridad y la claridad de nombres
- Evita nombres similares que puedan confundir
- Revisa las consultas en el código y en los PRs
4) En Celery, los errores pueden convertirse en reintentos silenciosos
- Asegúrate de que los errores críticos no se traten como “temporal”
- Revisa la política de manejo de excepciones y los logs
Con estos puntos en mente, podrás evitar perder días de trabajo por un error que parece un problema de red.

No hay comentarios.