#21330 optimize object tag creation

This commit is contained in:
Arthur
2026-03-05 09:34:00 -08:00
parent fa5f9430fc
commit 0ea353eed3
3 changed files with 71 additions and 1 deletions

65
netbox/extras/managers.py Normal file
View File

@@ -0,0 +1,65 @@
from django.db.models import signals
from django.db import router
from taggit.managers import _TaggableManager
__all__ = (
'NetBoxTaggableManager',
)
class NetBoxTaggableManager(_TaggableManager):
"""
Extends taggit's _TaggableManager to replace the per-tag get_or_create loop in add() with a
single bulk_create() call, reducing SQL queries from O(N) to O(1) when assigning tags.
"""
def add(self, *tags, through_defaults=None, tag_kwargs=None, **kwargs):
self._remove_prefetched_objects()
if tag_kwargs is None:
tag_kwargs = {}
tag_objs = self._to_tag_model_instances(tags, tag_kwargs)
new_ids = {t.pk for t in tag_objs}
# Determine which tags are not already assigned to this object
vals = set(
self.through._default_manager.using(db)
.values_list("tag_id", flat=True)
.filter(**self._lookup_kwargs())
)
new_ids -= vals
if not new_ids:
return
db = router.db_for_write(self.through, instance=self.instance)
signals.m2m_changed.send(
sender=self.through,
action="pre_add",
instance=self.instance,
reverse=False,
model=self.through.tag_model(),
pk_set=new_ids,
using=db,
)
# Use a single bulk INSERT instead of one get_or_create per tag.
lookup = self._lookup_kwargs()
self.through._default_manager.using(db).bulk_create(
[
self.through(tag=tag, **lookup, **(through_defaults or {}))
for tag in tag_objs
if tag.pk in new_ids
],
ignore_conflicts=True,
)
signals.m2m_changed.send(
sender=self.through,
action="post_add",
instance=self.instance,
reverse=False,
model=self.through.tag_model(),
pk_set=new_ids,
using=db,
)

View File

@@ -53,8 +53,11 @@ class TaggableModelSerializer(serializers.Serializer):
def _save_tags(self, instance, tags):
if tags:
# Cache tags on instance so serialize_object() can reuse them without a DB query
instance._tags = tags
instance.tags.set([t.name for t in tags])
else:
instance._tags = []
instance.tags.clear()
return instance

View File

@@ -9,6 +9,7 @@ from django.db import models
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from extras.managers import NetBoxTaggableManager
from taggit.managers import TaggableManager
from core.choices import JobStatusChoices, ObjectChangeActionChoices
@@ -487,11 +488,12 @@ class JournalingMixin(models.Model):
class TagsMixin(models.Model):
"""
Enables support for tag assignment. Assigned tags can be managed via the `tags` attribute,
which is a `TaggableManager` instance.
which is a `NetBoxTaggableManager` instance.
"""
tags = TaggableManager(
through='extras.TaggedItem',
ordering=('weight', 'name'),
manager=NetBoxTaggableManager,
)
class Meta: