Opened 4 years ago
Closed 8 months ago
#33579 closed New feature (fixed)
Raise a specialized exception when Model.save(update_fields) does not affect any rows
| Reported by: | Simon Charette | Owned by: | Simon Charette | 
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | 4.0 | 
| Severity: | Normal | Keywords: | |
| Cc: | Sardorbek Imomaliev, Mariusz Felisiak | Triage Stage: | Ready for checkin | 
| Has patch: | yes | Needs documentation: | no | 
| Needs tests: | no | Patch needs improvement: | no | 
| Easy pickings: | no | UI/UX: | no | 
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 , 4 years ago
| Description: | modified (diff) | 
|---|
comment:2 by , 4 years ago
| Triage Stage: | Unreviewed → Accepted | 
|---|
comment:3 by , 4 years ago
comment:4 by , 4 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 , 4 years ago
| Cc: | added | 
|---|
comment:6 by , 4 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 , 17 months ago
| Has patch: | set | 
|---|
comment:9 by , 17 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 , 17 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 , 15 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 , 14 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 , 8 months 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.ObjectNotUpdatedandNotUpdated.
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 , 8 months 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 , 8 months ago
| Owner: | changed from to | 
|---|
comment:16 by , 8 months ago
| Needs documentation: | unset | 
|---|---|
| Needs tests: | unset | 
| Patch needs improvement: | unset | 
comment:17 by , 8 months 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
ProgrammingErrororInterfaceError