Django 1.10 error on add new instance
MikeVL opened this issue · comments
I have model with PositionField. On create instance i got error:
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/usr/local/lib/python2.7/dist-packages/django/db/models/base.py", line 796, in save
force_update=force_update, update_fields=update_fields)
File "/usr/local/lib/python2.7/dist-packages/django/db/models/base.py", line 824, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/usr/local/lib/python2.7/dist-packages/django/db/models/base.py", line 880, in _save_table
raise ValueError("Cannot force an update in save() with no primary key.")
ValueError: Cannot force an update in save() with no primary key.
Confirm, same here
Have the same. I have spend some time to debug the problem, and I know what causes the issue, but cannot figure out how to fix it. So I will share what I discovered.
Everything breaks because of the change in implementation of get_deferred_fields
method, in Django Model class (django.db.models.Model). This method is called from save()
method of Model class see here.
It used to be like that (Django 1.9):
Source
def get_deferred_fields(self):
"""
Returns a set containing names of deferred fields for this instance.
"""
return {
f.attname for f in self._meta.concrete_fields
if isinstance(self.__class__.__dict__.get(f.attname), DeferredAttribute)
}
But now it is (Django 1.10):
Source
def get_deferred_fields(self):
"""
Returns a set containing names of deferred fields for this instance.
"""
return {
f.attname for f in self._meta.concrete_fields
if f.attname not in self.__dict__
}
The problem is that self.__dict__
does not contain our PositionField attribute, what causes the position attribute to be treated as deferred. Instead the self.__dict__
contains position attribute cache (deeper understanding of PositionField is needed).
Going a little bit deeper brought me to contribute_to_class
method of PositionField class and one particular line setattr(cls, self.name, self)
:
Source
def contribute_to_class(self, cls, name):
super(PositionField, self).contribute_to_class(cls, name)
for constraint in cls._meta.unique_together:
if self.name in constraint:
raise TypeError("%s can't be part of a unique constraint." % self.__class__.__name__)
self.auto_now_fields = []
for field in cls._meta.fields:
if getattr(field, 'auto_now', False):
self.auto_now_fields.append(field)
setattr(cls, self.name, self)
pre_delete.connect(self.prepare_delete, sender=cls)
post_delete.connect(self.update_on_delete, sender=cls)
post_save.connect(self.update_on_save, sender=cls)
That line causes the position attribute to be missing from self.__dict__
while checking for deferred fields. But this line also somehow sets the position attribute cache field, which is required for PositionField to work. The self.name
is carries the attribute name of PositionFiled as we define it in the class like so:
position = PositionField()
The self.name
will contain the 'position'
string.
(Not sure how it set's the cache attribute, if all it does it sets the position attribute #confused)
Few last things I came along:
As the documentation states there is a way that class attributes differ from the __dict__
See section Implementing Descriptors for another way in which attributes of a class retrieved via its instances may differ from the objects actually stored in the class’s __dict__.
Source, look for "Class instances" section.
But I don't really know how it work and if it is relevant in this case.
A workaround for that can be very simple, just override the get_deferred_fields
in your class and make sure your position attribute is never deferred. But it is not a good way to solve this problem:
def get_deferred_fields(self):
deferred_set = super().get_deferred_fields()
return {f for f in deferred_set if f != 'position'}
Assuming the position attribute in model class is called position
.
New deferred logic indeed looks strange. Added workaround for that. Tested locally.
Hi, I had a similar error just by overriding save method and overriding get_deferred_fields worked as a workaround. So thanks! Have you considered reporting that to Django?
I faced the same issue and also for me overriding get_deferred_fields worked! Thanks a lot @akszydelko :)
Thank you @akszydelko this helped us immensely