Opened 3 years ago
Closed 5 weeks ago
#33579 closed New feature (fixed)
Raise a specialized exception when Model.save(update_fields) does not affect any rows
Description (last modified by ) ¶
When Model.save(update_fields)
is used to update an instance of a previously fetched model and the resulting UPDATE
doesn't affect any row a DatabaseError
is raised.
Since the resulting exception cannot be differentiated by its type
from any other dababase errors occuring during save
(e.g. a failed UPDATE
also results in a DatabaseError
) the only pattern to gracefully handle this rare edge case to catch all DatabaseError
and compare its .args[0]
(or str
representation) to a string that doesn't offer any stability guarantees.
In order to make it easier for advanced users that rely on the update_fields
feature to handle this case differently from others where a DatabaseError
is raised I propose that that we introduce a new DatabaseError
subclass that would be raised instead when an unexpected empty update occurs.
I believe both instances should be switched to this pattern (force_update
and update_fields
).
Change History (18)
comment:1 by , 3 years ago
Description: | modified (diff) |
---|
comment:2 by , 3 years ago
Triage Stage: | Unreviewed → Accepted |
---|
comment:3 by , 3 years ago
comment:4 by , 3 years ago
I feel like a django.db.models.Model.save
forced update failure has little to do with PEP-249; it's an exception that occurs at the ORM layer and not at the backend one.
Not sure if we should raise a ProgrammingError or InterfaceError
I would argue that we should not subclass either of them as it will result in the same effect as raising DatabaseError
from a callers perspective (no way to differentiate between a database level error and an ORM level one without a str(exc)
comparison).
The reason for proposing to subclass DatabaseError
is solely for backward compatibility purposes which I believed would make the proposed change less controversial.
If it was do all over again I think we should have a dedicated NoopUpdate
(better name welcome) type like we do with DoesNotExist
and MultipleObjectsReturned
but since Python doesn't allow for virtual subclass overrides in except clauses there's no way to provide a deprecation path for except DatabaseError
clauses in the wild that might already be handling this case.
comment:5 by , 3 years ago
Cc: | added |
---|
comment:6 by , 3 years ago
Owner: | changed from | to
---|---|
Status: | new → assigned |
comment:7 by , 3 years ago
Another alternative we would be to simply raise a cls.DoesNotExists
error instead.
It would be backward incompatible in the sense that a different exception type would be raised but it seems it would fit elegantly with the message it's meant to convey; the object that was attempted to be updated was not found. It would also make code that handles generic DoesNotExist
and turn them into HTTP 404 automatically pick up errors of this form without any special casing.
Another argument for this approach is that the affected base is already available from Model._save_table
via the cls
kwarg so it would also allow to differentiate which model row was missing when dealing with concrete model inheritance that require update against multiple tables.
comment:8 by , 10 months ago
Has patch: | set |
---|
comment:9 by , 10 months ago
I've made a PR for this which simply creates a new exception by inheriting from DatabaseError. This will be compatible with people who already was catching DatabaseError, and will make new developments easier by having a separate exception class.
comment:10 by , 10 months ago
Patch needs improvement: | set |
---|
Left some comments on the PR about possible improvement to make the raised exception more coherent with ObjectDoesNotExist
and MultipleObjectsReturned
while still inheriting from DatabaseError
for backward compatiblity reasons.
comment:11 by , 8 months ago
Patch needs improvement: | unset |
---|
I've done all request changes, and now I think now it's ready to be merged
follow-up: 13 comment:12 by , 8 months ago
Needs documentation: | set |
---|---|
Needs tests: | set |
Patch needs improvement: | set |
Left some comments on the PR. It still needs tests tweaking, some code adjustments, and documentation about django.core.exceptions.ObjectNotUpdated
and NotUpdated
.
comment:13 by , 7 weeks ago
Cc: | added |
---|
Replying to Simon Charette:
Left some comments on the PR. It still needs tests tweaking, some code adjustments, and documentation about
django.core.exceptions.ObjectNotUpdated
andNotUpdated
.
TBH, this error is extremely annoying to me. It forces users to catch DatabaseError
on every save()
call when you process things asynchronously and don't really care if someone was deleted in the meantime. I'd vote to remove it totally 😉, we don't raise any exception on save()
without update_fields
.
comment:14 by , 5 weeks ago
I'd vote to remove it totally 😉, we don't raise any exception on save() without update_fields.
As annoying as this exception can be I don't think we should entirely silence it. Passing update_fields
is synonymous with force_update
and just like when force_insert
is provided erroring out to let callers know that the forced operation failed seems like the right thing to do as doing otherwise could result in unexpected data loses. Any save()
call can raise an IntegrityError
(and other flavors of DatabaseError
) so even if we removed that particular instance it would still be best practice to handle them on save
calls.
At least with a specialized exception, something less generic than DatabaseError
, callers can more adequately react to such errors. For example, raised ObjectNotUpdated
can be turned into a 404, silenced, or turn into a subsequent save
call without update fields depending on the situation.
If you look at the core usages of save(update_fields)
there seems to be cases where we'd want to do one over the other. For example, reverse_many_to_one_manager._clear
might want to silence such exceptions when assigning None
while in RenameContentType._rename
you'd most like would want to refetch the row by natural key and update it again.
Once the exception lands and is documented it could allow third party tools like DRF to catch them on PUT
/ PATCH
and turn them into 404s.
Given the patch has not been improved for over 6 months I'll assign it to me and try to bring it through the finish line.
comment:15 by , 5 weeks ago
Owner: | changed from | to
---|
comment:16 by , 5 weeks ago
Needs documentation: | unset |
---|---|
Needs tests: | unset |
Patch needs improvement: | unset |
comment:17 by , 5 weeks ago
Triage Stage: | Accepted → Ready for checkin |
---|
Is it the best the idea to introduce a new subclass rather than using one of the other ones ? Because I think we should stick with the ones specified in PEP 249.
Not sure if we should raise a
ProgrammingError
orInterfaceError