Opened 3 weeks ago

Closed 2 weeks ago

Last modified 2 weeks ago

#36855 closed Bug (fixed)

Field.pre_save() called twice during save() in Django 6.0 when inserting new records

Reported by: Nino Walker Owned by: Kundan Yadav
Component: Documentation Version: 6.0
Severity: Release blocker Keywords: pre_save
Cc: Nino Walker, Simon Charette 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

Commit 94680437 introduced a change where Field.pre_save() can be called twice during Model.save() for new records:

Diff: https://github.com/django/django/commit/94680437a45a71c70ca8bd2e68b72aa1e2eff337
Full method: https://github.com/django/django/blob/94680437a45a71c70ca8bd2e68b72aa1e2eff337/django/db/models/base.py#L1085-L1164

Previously, pre_save() was only called once in the values comprehension.

Impact:

This is a breaking change for custom model fields that:

  • Have side effects in pre_save() (e.g., registering transaction.on_commit callbacks)
  • Transform the value in a way that's not idempotent
  • Maintain internal state based on pre_save() being called once per save

Example:

A custom field that registers an on_commit callback in pre_save() will now register it twice, causing the callback to execute twice.

Expected behavior:

Field.pre_save() should be called exactly once per Model.save() operation, regardless of whether the save results in an INSERT or UPDATE.

Change History (13)

comment:1 by Jacob Walls, 3 weeks ago

Component: Database layer (models, ORM)Documentation
Keywords: pre_save added
Severity: NormalRelease blocker
Triage Stage: UnreviewedAccepted

Thanks for the report. This was discussed just a couple days ago, but I'm happy to have a new ticket for clarity.

I suggested documenting that pre_save() functions should be idempotent in ticket:36847#comment:11, but Simon pointed out we should update our own DateField.pre_save implementations first. (They aren't technically idempotent, but they're pretty close. They're certainly side-effect free.)

Some of the use cases you mention seem a better fit for the pre_save signal than the pre_save() field method, but I'd have to look closer. Committing to support for non-idempotent pre_save() methods is a new feature that would merit some discussion elsewhere on the forum or django/new-features. Take the example of another field method: ArrayField.get_db_prep_value calls get_db_prep_value() on each of its base fields. For that reason if get_db_prep_value() had side effects, that would be a problem.

We likely should have documented this as a minor incompatible change in 6.0, so I'm reframing this as a documentation fix for the release notes for now. Django has a grain, and one way to follow the grain is ensure that pre_save() only preprocesses single values without side effects. Side effects can be orchestrated in many other ways.

comment:2 by Jacob Walls, 3 weeks ago

Cc: Simon Charette added

comment:3 by Nino Walker, 3 weeks ago

Thank you for the prompt reply. The specific context was with the django-gcp library, which I was testing for compatibility with 6.0. I haven't been in the Django internals since 1.2, so I won't argue the virtues of your position. Below is my exact context workaround:

https://github.com/ninowalker/django-gcp/pull/2/changes

Specific workaround: https://github.com/ninowalker/django-gcp/pull/2/changes#diff-d92dddc7dd622b9917b9a6f5b0a4dee29f5c8d5c1870831953399c681626d9ac

Thanks!

Here's the implementation of the storage field: https://github.com/octue/django-gcp/blob/main/django_gcp/storage/fields.py (from the upstream repository)

Last edited 3 weeks ago by Nino Walker (previous) (diff)

comment:4 by HarshAlreja, 3 weeks ago

Has patch: set

comment:5 by Jacob Walls, 3 weeks ago

Has patch: unset

PR didn't target release notes.

comment:6 by Kundan Yadav, 2 weeks ago

hey can i work on it ?

comment:7 by Jacob Walls, 2 weeks ago

What we need is a single sentence added under the Miscellaneous section under Backwards incompatible changes in 6.0 in 6.0.txt. If that sounds appealing, go right ahead.

comment:8 by Kundan Yadav, 2 weeks ago

Owner: set to Kundan Yadav
Status: newassigned

comment:10 by Jacob Walls, 2 weeks ago

Patch needs improvement: set

The wrong file was edited.

comment:11 by Jacob Walls, 2 weeks ago

Patch needs improvement: unset
Triage Stage: AcceptedReady for checkin

comment:12 by Jacob Walls <jacobtylerwalls@…>, 2 weeks ago

Resolution: fixed
Status: assignedclosed

In 92415607:

Fixed #36855, Refs #27222 -- Mentioned multiple invocations of Field.pre_save() in 6.0 release notes.

Co-authored-by: Jacob Walls <jacobtylerwalls@…>

comment:13 by Jacob Walls <jacobtylerwalls@…>, 2 weeks ago

In 8027c0a:

[6.0.x] Fixed #36855, Refs #27222 -- Mentioned multiple invocations of Field.pre_save() in 6.0 release notes.

Co-authored-by: Jacob Walls <jacobtylerwalls@…>

Backport of 924156072ecf61ef9cf50fa0d4d553a4c0d416c2 from main.

Note: See TracTickets for help on using tickets.
Back to Top