-
-
Notifications
You must be signed in to change notification settings - Fork 6.9k
Skip unnecessary unique together checking #9154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Skip unnecessary unique together checking #9154
Conversation
] | ||
if None not in checked_values and qs_exists(queryset): | ||
if serializer.instance is None: | ||
checked_values = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will this change have any effect on schema migrations? thanks for the patch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @auvipy I believe there won't be any changes to the schema or any impact on migrations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to know more context and use cases here and if this change will remain backward compatible? if not we should document that and make a strong case for the inclusion of the change
Hi @auvipy, thanks for reviewing. First, I can confirm that this change will remain backward compatible. Currently, the validator checks the unique-together fields by querying the database before saving, regardless of whether the field values have changed or not. This approach incurs unnecessary costs. Therefore, I compared the new and existing values, and now, the unique validation is performed only if the related fields have changed. |
That sounds good |
Is it possible to add additional tests for this? |
@@ -447,6 +447,22 @@ def test_do_not_ignore_validation_for_null_fields(self): | |||
serializer = NullUniquenessTogetherSerializer(data=data) | |||
assert not serializer.is_valid() | |||
|
|||
def test_ignore_validation_for_unchanged_fields(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@auvipy This is a new test I added for the scenario where none of the fields in the unique-together constraint are changed. For other cases, such as when any of the field values change, they are already covered by other test cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was asking for if more additional tests could be provided?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, though I believe all the cases have already been covered, I might have overlooked something. Could you suggest what types of additional tests might be needed?
Upgrading from 3.14.0 to 3.15.1 caused tests for a project of mine to break, and I think it's related to or because of this PR. However, I don't know that I would consider that a regression for DRF, and I was able to find a workaround (see below). Example CodeI apologize in advance, I can only post redacted example code. Model: class RedactedModel(models.Model):
...
fk = models.ForeignKey(...)
status = models.CharField(..., default="not-primary-1")
usage = models.CharField(...)
class Meta:
constraints = [
models.UniqueConstraint(
name="usage_unique_status_idx",
fields=["fk", "usage"],
condition=models.Q(status="primary"),
),
] Serializer: class RedactedModelSerializer(serializers.HyperlinkedModelSerializer):
...
fk = serializers.HyperlinkedRelatedField(
queryset=FKModel.objects.all(),
...,
)
status = serializers.ReadOnlyField(default="not-primary-1")
class Meta:
model = RedactedModel
fields = ['fk', 'status', 'usage', ...] ViewSet: class RedactedModelViewSet(viewsets.ModelViewSet):
queryset = RedactedModel.objects.all()
serializer_class = RedactedModelSerializer
...
def perform_create(self, serializer, *args, **kwargs):
with transaction.atomic():
try:
RedactedModel.objects.filter(
fk=serializer.validated_data['fk'],
status="primary",
usage=serializer.validated_data['usage'],
).update(status="not-primary-2")
except IntegrityError:
...
serializer.save() I'm not 100% sure that I am doing everything correctly, so if there's something obviously amiss, or even a better way to implement this, please let me know. The ProblemFor quick reference, here's some relevant DRF code: class CreateModelMixin:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) Originally (eg: with DRF 3.14.0), serializer validation seems to have skipped checking the unique together fields, which allowed my custom After updating to 3.15.1, the serializer validation no longer skips checking the unique together fields, so the serializer never validates the data, previous RedactedModel objects are never updated, and a new RedactedModel is not created. The SolutionI found a clue in class ModelSerializer(Serializer):
def get_validators(self):
"""
Determine the set of validators to use when instantiating serializer.
"""
# If the validators have been declared explicitly then use that.
validators = getattr(getattr(self, 'Meta', None), 'validators', None)
if validators is not None:
return list(validators)
# Otherwise use the default set of validators.
return (
self.get_unique_together_validators() +
self.get_unique_for_date_validators()
) It's a bit of an imperfect workaround, but I set the validators to an empty list in the serializer class RedactedModelSerializer(serializers.HyperlinkedModelSerializer):
...
class Meta:
model = RedactedModel
# Turn off unique together validators so we can validate serialized data
# and handle non-unique situations manually during creation
validators = []
... This bypasses serializer validation for the unique constraints so the update code in I'm not sure that this workaround is reasonable for other use cases, but I'm also not sure if this is uncovering a bug in a previous version (3.14.0), if it is uncovering a regression somewhere between 3.14.0 and 3.15.1, or if it is uncovering a bug in my codebase. But I hope this might help somebody else who runs into this. |
@blag I'd suggest opening a new issue with this report (otherwise it will get lost). If you could reduce it to a test case against the DRF test suite that shows the change in behaviour that would help significantly. (It's not immediately clear from the code you've posted, so needs further investigation to be actionable.) Thanks. |
This reverts commit 41edb3b. Tests not included.
upgrade `django-rest-framework` to `3.16.0` The reverted commit is purely an optimization which unfortunately breaks authentik, specifically Blueprints. It adds `getattr(serializer.instance, field)` to a validator. If `field` is a `RelatedObject`, that invocation queries the database. When authentik creates objects using Blueprints, it doesn't place related objects into the database before the validator tries to get them from there, so with the reverted commit, it produces `RelatedObjectDoesNotExist`. Perhaps a long-term solution is to revise how Blueprints work, or perhaps it is to change upstream. But in the meantime, Django 5.0 support ended and upgrading to Django 5.1 requires an upgrade of `django-rest-framework` to `3.16.0`, hence this workaround. See - encode/django-rest-framework#9154 - encode/django-rest-framework#9358 - encode/django-rest-framework#9482 - encode/django-rest-framework#9483
Description
Skip unnecessary unique-together validations when all unique field values remain unchanged.
Currently, the validator checks the unique-together fields by querying the database before saving, regardless of whether the field values have changed or not. This approach incurs unnecessary costs. Therefore, I compared the new and existing values, and now, the unique validation is performed only if the related fields have changed.