python - Determine si un atributo es un `DeferredAttribute` en django
django-models django-orm (2)
El contexto
He localizado un error bastante crítico en la Máquina de caché de Django que hace que su lógica de invalidación se vuelva loca después de una actualización de Django 1.4 a 1.7.
El error se localiza en invocaciones de only()
en modelos que extienden CachingMixin
la máquina de CachingMixin
. Resulta en recursiones profundas que ocasionalmente rompen la pila, pero por lo demás crean enormes flush_lists
que la máquina de caché usa para la invalidación bidireccional para modelos en relaciones ForeignKey
.
class MyModel(CachingMixin):
id = models.CharField(max_length=50, blank=True)
nickname = models.CharField(max_length=50, blank=True)
favorite_color = models.CharField(max_length=50, blank=True)
content_owner = models.ForeignKey(OtherModel)
m = MyModel.objects.only(''id'').all()
El bicho
El error se produce en las siguientes líneas ( https://github.com/jbalogh/django-cache-machine/blob/f827f05b195ad3fc1b0111131669471d843d631f/caching/base.py#L253-L254 ). En este caso, self
es una instancia de MyModel
con una combinación de atributos diferidos y no diferidos:
fks = dict((f, getattr(self, f.attname)) for f in self._meta.fields
if isinstance(f, models.ForeignKey))
La máquina de caché realiza la invalidación bidireccional en las relaciones de ForeignKey
. Para ello, recorre todos los campos de un Model
y almacena una serie de punteros en la memoria caché que apuntan a objetos que deben invalidarse cuando el objeto en cuestión se invalida.
El uso de only()
en el ORM de Django hace algo de programación mágica que anula los atributos no alcanzados con la implementación del atributo DeferredAttribute
de Django. En circunstancias normales, un acceso a favorite_color
invocaría a DeferredAttribute.__get__
( https://github.com/django/django/blob/18f3e79b13947de0bda7c985916d5a04e28936dc/django/db/models/query_utils.py#L121-L146 . caché de resultados o la fuente de datos. Para ello, busca la representación no diferida del Model
en cuestión y llama a otra consulta only()
en él.
Este es el problema cuando se pasa por encima de las claves foráneas en el Model
y se accede a sus valores, Cachine Machine introduce una recursión involuntaria. getattr(self, f.attname)
en un atributo que se aplaza induce la búsqueda de un Model
que tiene CachingMixin
aplicado y tiene atributos aplazados. Esto inicia de nuevo todo el proceso de almacenamiento en caché.
La pregunta
Me gustaría abrir un PR para solucionar este problema y creo que la respuesta es tan simple como saltarse los atributos diferidos, pero no estoy seguro de cómo hacerlo porque el acceso al atributo hace que se inicie el proceso de búsqueda.
Si todo lo que tengo es un identificador de una instancia de un Model
con una combinación de atributos diferidos y no diferidos, ¿hay una manera de determinar si un atributo es un atributo DeferredAttribute
sin acceder a él?
fks = dict((f, getattr(self, f.attname)) for f in self._meta.fields
if (isinstance(f, models.ForeignKey) and <f''s value isn''t a Deferred attribute))
Aquí es cómo verificar si un campo es diferido:
from django.db.models.query_utils import DeferredAttribute
is_deferred = isinstance(model_instance.__class__.__dict__.get(field.attname), DeferredAttribute):
Tomado de: https://github.com/django/django/blob/1.9.4/django/db/models/base.py#L393
Esto comprobará si el atributo es un atributo diferido y aún no se ha cargado desde la base de datos:
fks = dict((f, getattr(self, f.attname)) for f in self._meta.fields
if (isinstance(f, models.ForeignKey) and f.attname in self.__dict__))
Internamente, type(self)
es un modelo Proxy recién creado para la clase original. Un DeferredAttribute
primero verifica el dictado local de la instancia. Si eso no existe, cargará el valor de la base de datos. Este método omite el descriptor del objeto DeferredAttribute
para que el valor no se cargue si no existe.
Esto funciona en Django 1.4 y 1.7, y presumiblemente en las versiones intermedias. Tenga en cuenta que Django 1.8 introducirá a su debido tiempo el método get_deferred_fields()
que reemplazará todo este entrometimiento con los elementos internos de la clase.