Opened 3 days ago
Last modified 17 hours ago
#36064 assigned Bug
save() behavior for new model instance with default primary key does not apply to CompositePrimaryKey
Reported by: | Jacob Walls | Owned by: | Csirmaz Bendegúz |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | dev |
Severity: | Release blocker | Keywords: | |
Cc: | Csirmaz Bendegúz | Triage Stage: | Accepted |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
Based on the description of how save()
is different than update_or_create()
if the model's primary key has a default, i.e. that only an INSERT is attempted if the pk is not set by the user, I would expect models with a CompositePrimaryKey to behave as if they "have a default" if all of their composed fields have a default, or else for this to be called out in the doc.
This test shows this is not the case. The first test showing 2 queries instead of 1 is not such a serious problem, but the second test not raising IntegrityError seems like it could bite a user who was expecting to catch IntegrityError and do a custom update (based on how other models behave since Django 3.0 and #29260).
-
tests/composite_pk/models/tenant.py
diff --git a/tests/composite_pk/models/tenant.py b/tests/composite_pk/models/tenant.py index ac0b3d9715..2273908031 100644
a b 1 import uuid 1 2 from django.db import models 2 3 3 4 … … class Comment(models.Model): 46 47 47 48 class Post(models.Model): 48 49 pk = models.CompositePrimaryKey("tenant_id", "id") 49 tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE )50 id = models.UUIDField( )50 tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, default=1) 51 id = models.UUIDField(default=uuid.uuid4) -
tests/composite_pk/test_create.py
diff --git a/tests/composite_pk/test_create.py b/tests/composite_pk/test_create.py index 7c9925b946..af7ceb7df2 100644
a b 1 from django.db import IntegrityError 1 2 from django.test import TestCase 2 3 3 from .models import Tenant, User4 from .models import Post, Tenant, User 4 5 5 6 6 7 class CompositePKCreateTests(TestCase): … … class CompositePKCreateTests(TestCase): 14 15 id=1, 15 16 email="user0001@example.com", 16 17 ) 18 cls.post = Post.objects.create() 19 20 def test_save_default_pk_not_set(self): 21 with self.assertNumQueries(1): 22 Post().save() 23 24 def test_save_default_pk_set(self): 25 with self.assertRaises(IntegrityError): 26 Post(tenant_id=1, id=self.post.id).save() 27 17 28 18 29 def test_create_user(self): 19 30 test_cases = (
Change History (6)
comment:1 by , 43 hours ago
Cc: | added |
---|---|
Triage Stage: | Unreviewed → Accepted |
follow-up: 3 comment:2 by , 33 hours ago
This is my first time trying to submit code. Please see if it can meet the requirements. If so, I will learn to submit PR. Thanks
-
django/db/models/base.py
diff --git a/django/db/models/base.py b/django/db/models/base.py index a7a26b405c..87c19fd887 100644
a b class ModelState: 455 455 # explicit (non-auto) PKs. This impacts validation only; it has no effect 456 456 # on the actual save. 457 457 adding = True 458 is_set_composite_pk = False 458 459 fields_cache = ModelStateFieldsCacheDescriptor() 459 460 460 461 … … class Model(AltersData, metaclass=ModelBase): 584 585 new = cls(*values) 585 586 new._state.adding = False 586 587 new._state.db = db 588 if new._meta.is_composite_pk: 589 new._state.is_set_composite_pk = True 587 590 return new 588 591 589 592 def __repr__(self): … … class Model(AltersData, metaclass=ModelBase): 1104 1107 if f.name in update_fields or f.attname in update_fields 1105 1108 ] 1106 1109 1107 if not self._is_pk_set(meta): 1108 pk_val = meta.pk.get_pk_value_on_save(self) 1109 setattr(self, meta.pk.attname, pk_val) 1110 pk_set = self._is_pk_set(meta) 1110 if meta.is_composite_pk and not (force_update or update_fields): 1111 pk_set = self._state.is_set_composite_pk 1112 else: 1113 pk_set = self._is_pk_set(meta) 1114 if not pk_set: 1115 pk_val = meta.pk.get_pk_value_on_save(self) 1116 setattr(self, meta.pk.attname, pk_val) 1117 pk_set = self._is_pk_set(meta) 1118 1111 1119 if not pk_set and (force_update or update_fields): 1112 1120 raise ValueError("Cannot force an update in save() with no primary key.") 1113 1121 updated = False -
django/db/models/fields/composite.py
diff --git a/django/db/models/fields/composite.py b/django/db/models/fields/composite.py index 2b196f6d2a..3ab8295dbc 100644
a b class CompositeAttribute: 28 28 attnames = self.attnames 29 29 length = len(attnames) 30 30 31 if values is None: 31 is_clear_cp = values is None 32 if is_clear_cp: 32 33 values = (None,) * length 33 34 34 35 if not isinstance(values, (list, tuple)): … … class CompositeAttribute: 38 39 39 40 for attname, value in zip(attnames, values): 40 41 setattr(instance, attname, value) 42 instance._state.is_set_composite_pk = is_clear_cp 41 43 42 44 43 45 class CompositePrimaryKey(Field): -
tests/composite_pk/models/tenant.py
diff --git a/tests/composite_pk/models/tenant.py b/tests/composite_pk/models/tenant.py index ac0b3d9715..1c1108c0d3 100644
a b 1 import uuid 2 1 3 from django.db import models 2 4 3 5 … … class Comment(models.Model): 46 48 47 49 class Post(models.Model): 48 50 pk = models.CompositePrimaryKey("tenant_id", "id") 49 tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE )50 id = models.UUIDField( )51 tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, default=1) 52 id = models.UUIDField(default=uuid.uuid4) -
tests/composite_pk/test_create.py
diff --git a/tests/composite_pk/test_create.py b/tests/composite_pk/test_create.py index 7c9925b946..5120324f65 100644
a b 1 1 from django.test import TestCase 2 2 3 from .models import Tenant, User 3 from .models import Tenant, User, Post 4 4 5 5 6 6 class CompositePKCreateTests(TestCase): … … class CompositePKCreateTests(TestCase): 14 14 id=1, 15 15 email="user0001@example.com", 16 16 ) 17 cls.post = Post.objects.create() 18 19 def test_save_default_pk_not_set(self): 20 with self.assertNumQueries(1): 21 Post().save() 17 22 18 23 def test_create_user(self): 19 24 test_cases = ( -
tests/composite_pk/test_filter.py
diff --git a/tests/composite_pk/test_filter.py b/tests/composite_pk/test_filter.py index 7e361c5925..9fa3e4edb1 100644
a b class CompositePKFilterTests(TestCase): 37 37 cls.comment_4 = Comment.objects.create(id=4, user=cls.user_3) 38 38 cls.comment_5 = Comment.objects.create(id=5, user=cls.user_1) 39 39 40 40 41 def test_filter_and_count_user_by_pk(self): 41 42 test_cases = ( 42 43 ({"pk": self.user_1.pk}, 1),
comment:3 by , 19 hours ago
Replying to mao xin chen:
This is my first time trying to submit code. Please see if it can meet the requirements. If so, I will learn to submit PR. Thanks
Thanks for the patch! Please check the docs on how to contribute. If you open a PR on GitHub we can review your code.
comment:4 by , 19 hours ago
Thanks Jacob!
n | default | obj.save() | conflict |
1 | yes | insert | IntegrityError |
2 | no | update & insert | update |
- If pk has a default or db_default, saving a new object with a conflicting pk attempts an insert and raises an IntegrityError
- If pk doesn't have a default or db_default, saving a new object with a conflicting pk attempts an update (if no rows affected attempts an insert)
Is that correct?
# 1. Model.id has a default foo = Model(id=1) foo.save() # INSERT bar = Model(id=1) bar.save() # INSERT & IntegrityError # 2. Model.id doesn't have a default foo = Model(id=1) foo.save() # UPDATE & INSERT bar = Model(id=1) bar.save() # UPDATE
comment:5 by , 18 hours ago
Has patch: | set |
---|
comment:6 by , 17 hours ago
Owner: | set to |
---|---|
Status: | new → assigned |
Thank you Jacob! Agree this needs either a fix or some docs