diff --git a/django/db/models/query.py b/django/db/models/query.py index be0deb90b0..5673855c1c 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -20,7 +20,7 @@ from django.db import ( router, transaction, ) -from django.db.models import AutoField, DateField, DateTimeField, sql +from django.db.models import AutoField, DateField, DateTimeField, Field, sql from django.db.models.constants import LOOKUP_SEP, OnConflict from django.db.models.deletion import Collector from django.db.models.expressions import Case, F, Ref, Value, When @@ -963,7 +963,25 @@ class QuerySet: return obj, created for k, v in resolve_callables(defaults): setattr(obj, k, v) - obj.save(using=self.db) + + update_fields = set(defaults) + concrete_field_names = self.model._meta._non_pk_concrete_field_names + # update_fields does not support non-concrete fields. + if concrete_field_names.issuperset(update_fields): + # Add fields which are set on pre_save(), e.g. auto_now fields. + # This is to maintain backward compatibility as these fields + # are not updated unless explicitly specified in the + # update_fields list. + for field in self.model._meta.local_concrete_fields: + if not ( + field.primary_key or field.__class__.pre_save is Field.pre_save + ): + update_fields.add(field.name) + if field.name != field.attname: + update_fields.add(field.attname) + obj.save(using=self.db, update_fields=update_fields) + else: + obj.save(using=self.db) return obj, False async def aupdate_or_create(self, defaults=None, **kwargs): diff --git a/tests/get_or_create/models.py b/tests/get_or_create/models.py index f8f6157348..6875671501 100644 --- a/tests/get_or_create/models.py +++ b/tests/get_or_create/models.py @@ -63,3 +63,4 @@ class Book(models.Model): related_name="books", db_column="publisher_id_column", ) + updated = models.DateTimeField(auto_now=True) diff --git a/tests/get_or_create/tests.py b/tests/get_or_create/tests.py index aa68d41c00..65d7510682 100644 --- a/tests/get_or_create/tests.py +++ b/tests/get_or_create/tests.py @@ -6,6 +6,7 @@ from threading import Thread from django.core.exceptions import FieldError from django.db import DatabaseError, IntegrityError, connection from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature +from django.test.utils import CaptureQueriesContext from django.utils.functional import lazy from .models import ( @@ -513,6 +514,31 @@ class UpdateOrCreateTests(TestCase): self.assertIs(created, False) self.assertEqual(journalist.name, "John") + def test_update_only_defaults_and_pre_save_fields_when_local_fields(self): + publisher = Publisher.objects.create(name="Acme Publishing") + book = Book.objects.create(publisher=publisher, name="The Book of Ed & Fred") + + for defaults in [{"publisher": publisher}, {"publisher_id": publisher}]: + with self.subTest(defaults=defaults): + with CaptureQueriesContext(connection) as captured_queries: + book, created = Book.objects.update_or_create( + pk=book.pk, + defaults=defaults, + ) + self.assertIs(created, False) + update_sqls = [ + q["sql"] for q in captured_queries if q["sql"].startswith("UPDATE") + ] + self.assertEqual(len(update_sqls), 1) + update_sql = update_sqls[0] + self.assertIsNotNone(update_sql) + self.assertIn( + connection.ops.quote_name("publisher_id_column"), update_sql + ) + self.assertIn(connection.ops.quote_name("updated"), update_sql) + # Name should not be updated. + self.assertNotIn(connection.ops.quote_name("name"), update_sql) + class UpdateOrCreateTestsWithManualPKs(TestCase): def test_create_with_duplicate_primary_key(self):