Skip to content

Commit 55a0073

Browse files
charettesfelixxm
authored andcommitted
Refs #27222 -- Refreshed GeneratedFields values on save() initiated update.
This required implementing UPDATE RETURNING machinery that heavily borrows from the INSERT one.
1 parent c48904a commit 55a0073

12 files changed

Lines changed: 213 additions & 59 deletions

File tree

django/db/backends/base/features.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class BaseDatabaseFeatures:
3838
can_use_chunked_reads = True
3939
can_return_columns_from_insert = False
4040
can_return_rows_from_bulk_insert = False
41+
can_return_rows_from_update = False
4142
has_bulk_insert = True
4243
uses_savepoints = True
4344
can_release_savepoints = False

django/db/backends/oracle/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ def __init__(self, *args, **kwargs):
243243
"use_returning_into", True
244244
)
245245
self.features.can_return_columns_from_insert = use_returning_into
246+
self.features.can_return_rows_from_update = use_returning_into
246247

247248
@property
248249
def is_pool(self):

django/db/backends/oracle/features.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
1919
has_select_for_update_of = True
2020
select_for_update_of_column = True
2121
can_return_columns_from_insert = True
22+
can_return_rows_from_update = True
2223
supports_subqueries_in_group_by = False
2324
ignores_unnecessary_order_by_in_subqueries = False
2425
supports_tuple_comparison_against_subquery = False

django/db/backends/postgresql/features.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
1111
allows_group_by_selected_pks = True
1212
can_return_columns_from_insert = True
1313
can_return_rows_from_bulk_insert = True
14+
can_return_rows_from_update = True
1415
has_real_datatype = True
1516
has_native_uuid_field = True
1617
has_native_duration_field = True

django/db/backends/sqlite3/features.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,7 @@ def can_return_columns_from_insert(self):
171171
can_return_rows_from_bulk_insert = property(
172172
operator.attrgetter("can_return_columns_from_insert")
173173
)
174+
175+
can_return_rows_from_update = property(
176+
operator.attrgetter("can_return_columns_from_insert")
177+
)

django/db/models/base.py

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,12 +1094,28 @@ def _save_table(
10941094
]
10951095
forced_update = update_fields or force_update
10961096
pk_val = self._get_pk_val(meta)
1097-
updated = self._do_update(
1098-
base_qs, using, pk_val, values, update_fields, forced_update
1097+
returning_fields = [
1098+
f
1099+
for f in meta.local_concrete_fields
1100+
if (
1101+
f.generated
1102+
and f.referenced_fields.intersection(non_pks_non_generated)
1103+
)
1104+
]
1105+
results = self._do_update(
1106+
base_qs,
1107+
using,
1108+
pk_val,
1109+
values,
1110+
update_fields,
1111+
forced_update,
1112+
returning_fields,
10991113
)
1100-
if force_update and not updated:
1114+
if updated := bool(results):
1115+
self._assign_returned_values(results[0], returning_fields)
1116+
elif force_update:
11011117
raise self.NotUpdated("Forced update did not affect any rows.")
1102-
if update_fields and not updated:
1118+
elif update_fields:
11031119
raise self.NotUpdated(
11041120
"Save with update_fields did not affect any rows."
11051121
)
@@ -1131,11 +1147,19 @@ def _save_table(
11311147
cls._base_manager, using, fields, returning_fields, raw
11321148
)
11331149
if results:
1134-
for value, field in zip(results[0], returning_fields):
1135-
setattr(self, field.attname, value)
1150+
self._assign_returned_values(results[0], returning_fields)
11361151
return updated
11371152

1138-
def _do_update(self, base_qs, using, pk_val, values, update_fields, forced_update):
1153+
def _do_update(
1154+
self,
1155+
base_qs,
1156+
using,
1157+
pk_val,
1158+
values,
1159+
update_fields,
1160+
forced_update,
1161+
returning_fields,
1162+
):
11391163
"""
11401164
Try to update the model. Return True if the model was updated (if an
11411165
update query was done and a matching row was found in the DB).
@@ -1147,22 +1171,23 @@ def _do_update(self, base_qs, using, pk_val, values, update_fields, forced_updat
11471171
# case we just say the update succeeded. Another case ending up
11481172
# here is a model with just PK - in that case check that the PK
11491173
# still exists.
1150-
return update_fields is not None or filtered.exists()
1174+
if update_fields is not None or filtered.exists():
1175+
return [()]
1176+
return []
11511177
if self._meta.select_on_save and not forced_update:
1152-
return (
1153-
filtered.exists()
1154-
and
1155-
# It may happen that the object is deleted from the DB right
1156-
# after this check, causing the subsequent UPDATE to return
1157-
# zero matching rows. The same result can occur in some rare
1158-
# cases when the database returns zero despite the UPDATE being
1159-
# executed successfully (a row is matched and updated). In
1160-
# order to distinguish these two cases, the object's existence
1161-
# in the database is again checked for if the UPDATE query
1162-
# returns 0.
1163-
(filtered._update(values) > 0 or filtered.exists())
1164-
)
1165-
return filtered._update(values) > 0
1178+
# It may happen that the object is deleted from the DB right after
1179+
# this check, causing the subsequent UPDATE to return zero matching
1180+
# rows. The same result can occur in some rare cases when the
1181+
# database returns zero despite the UPDATE being executed
1182+
# successfully (a row is matched and updated). In order to
1183+
# distinguish these two cases, the object's existence in the
1184+
# database is again checked for if the UPDATE query returns 0.
1185+
if not filtered.exists():
1186+
return []
1187+
if results := filtered._update(values, returning_fields):
1188+
return results
1189+
return [()] if filtered.exists() else []
1190+
return filtered._update(values, returning_fields)
11661191

11671192
def _do_insert(self, manager, using, fields, returning_fields, raw):
11681193
"""
@@ -1177,6 +1202,10 @@ def _do_insert(self, manager, using, fields, returning_fields, raw):
11771202
raw=raw,
11781203
)
11791204

1205+
def _assign_returned_values(self, returned_values, returning_fields):
1206+
for value, field in zip(returned_values, returning_fields):
1207+
setattr(self, field.attname, value)
1208+
11801209
def _prepare_related_fields_for_save(self, operation_name, fields=None):
11811210
# Ensure that a model instance without a PK hasn't been assigned to
11821211
# a ForeignKey, GenericForeignKey or OneToOneField on this model. If

django/db/models/fields/generated.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ def generated_sql(self, connection):
6666
sql = f"CASE WHEN {sql} THEN 1 ELSE 0 END"
6767
return sql, params
6868

69+
@cached_property
70+
def referenced_fields(self):
71+
resolved_expression = self.expression.resolve_expression(
72+
self._query, allow_joins=False
73+
)
74+
referenced_fields = []
75+
for col in self._query._gen_cols([resolved_expression]):
76+
referenced_fields.append(col.target)
77+
return frozenset(referenced_fields)
78+
6979
def check(self, **kwargs):
7080
databases = kwargs.get("databases") or []
7181
errors = [

django/db/models/query.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,7 +1306,7 @@ async def aupdate(self, **kwargs):
13061306

13071307
aupdate.alters_data = True
13081308

1309-
def _update(self, values):
1309+
def _update(self, values, returning_fields=None):
13101310
"""
13111311
A version of update() that accepts field objects instead of field
13121312
names. Used primarily for model saving and not intended for use by
@@ -1320,7 +1320,9 @@ def _update(self, values):
13201320
# Clear any annotations so that they won't be present in subqueries.
13211321
query.annotations = {}
13221322
self._result_cache = None
1323-
return query.get_compiler(self.db).execute_sql(ROW_COUNT)
1323+
if returning_fields is None:
1324+
return query.get_compiler(self.db).execute_sql(ROW_COUNT)
1325+
return query.get_compiler(self.db).execute_returning_sql(returning_fields)
13241326

13251327
_update.alters_data = True
13261328
_update.queryset_only = False

django/db/models/sql/compiler.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,6 +2020,9 @@ def as_sql(self):
20202020

20212021

20222022
class SQLUpdateCompiler(SQLCompiler):
2023+
returning_fields = None
2024+
returning_params = ()
2025+
20232026
def as_sql(self):
20242027
"""
20252028
Create the SQL for this query. Return the SQL string and list of
@@ -2087,6 +2090,15 @@ def as_sql(self):
20872090
params = []
20882091
else:
20892092
result.append("WHERE %s" % where)
2093+
if self.returning_fields:
2094+
# Skip empty r_sql to allow subclasses to customize behavior for
2095+
# 3rd party backends. Refs #19096.
2096+
r_sql, self.returning_params = self.connection.ops.returning_columns(
2097+
self.returning_fields
2098+
)
2099+
if r_sql:
2100+
result.append(r_sql)
2101+
params.extend(self.returning_params)
20902102
return " ".join(result), tuple(update_params + params)
20912103

20922104
def execute_sql(self, result_type):
@@ -2110,6 +2122,38 @@ def execute_sql(self, result_type):
21102122
is_empty = False
21112123
return row_count
21122124

2125+
def execute_returning_sql(self, returning_fields):
2126+
"""
2127+
Execute the specified update and return rows of the returned columns
2128+
associated with the specified returning_field if the backend supports
2129+
it.
2130+
"""
2131+
if self.query.get_related_updates():
2132+
raise NotImplementedError(
2133+
"Update returning is not implemented for queries with related updates."
2134+
)
2135+
2136+
if (
2137+
not returning_fields
2138+
or not self.connection.features.can_return_rows_from_update
2139+
):
2140+
row_count = self.execute_sql(ROW_COUNT)
2141+
return [()] * row_count
2142+
2143+
self.returning_fields = returning_fields
2144+
with self.connection.cursor() as cursor:
2145+
sql, params = self.as_sql()
2146+
cursor.execute(sql, params)
2147+
rows = self.connection.ops.fetch_returned_rows(
2148+
cursor, self.returning_params
2149+
)
2150+
opts = self.query.get_meta()
2151+
cols = [field.get_col(opts.db_table) for field in self.returning_fields]
2152+
converters = self.get_converters(cols)
2153+
if converters:
2154+
rows = self.apply_converters(rows, converters)
2155+
return list(rows)
2156+
21132157
def pre_sql_setup(self):
21142158
"""
21152159
If the update depends on results from other tables, munge the "where"

docs/ref/models/fields.txt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,12 +1315,6 @@ materialized view.
13151315
PostgreSQL only supports persisted columns. Oracle only supports virtual
13161316
columns.
13171317

1318-
.. admonition:: Refresh the data
1319-
1320-
Since the database computes the value, the object must be reloaded to
1321-
access the new value after :meth:`~Model.save`, for example, by using
1322-
:meth:`~Model.refresh_from_db`.
1323-
13241318
.. admonition:: Database limitations
13251319

13261320
There are many database-specific restrictions on generated fields that
@@ -1338,6 +1332,12 @@ materialized view.
13381332
.. _PostgreSQL: https://www.postgresql.org/docs/current/ddl-generated-columns.html
13391333
.. _SQLite: https://www.sqlite.org/gencol.html#limitations
13401334

1335+
.. versionchanged:: 6.0
1336+
1337+
``GeneratedField``\s are now automatically refreshed from the database on
1338+
backends that support it (SQLite, PostgreSQL, and Oracle) and marked as
1339+
deferred otherwise.
1340+
13411341
``GenericIPAddressField``
13421342
-------------------------
13431343

0 commit comments

Comments
 (0)