summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/db/models/query.py22
-rw-r--r--tests/get_or_create/models.py1
-rw-r--r--tests/get_or_create/tests.py26
3 files changed, 47 insertions, 2 deletions
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):