Closes #22877: Improve caching logic when retrieving custom fields via get_for_model()

This commit is contained in:
Jeremy Stretch 2026-08-06 12:32:43 -04:00
parent e160359e6e
commit b1ee8297d2
12 changed files with 212 additions and 192 deletions

View File

@ -1,24 +1,24 @@
{
"circuit:api_list_objects": 16,
"circuit:list_objects_with_permission": 22,
"circuitgroup:api_list_objects": 13,
"circuitgroup:list_objects_with_permission": 20,
"circuit:api_list_objects": 15,
"circuit:list_objects_with_permission": 19,
"circuitgroup:api_list_objects": 12,
"circuitgroup:list_objects_with_permission": 17,
"circuitgroupassignment:api_list_objects": 17,
"circuitgroupassignment:list_objects_with_permission": 26,
"circuittermination:api_list_objects": 18,
"circuittermination:list_objects_with_permission": 24,
"circuittype:api_list_objects": 13,
"circuittype:list_objects_with_permission": 20,
"provider:api_list_objects": 15,
"provider:list_objects_with_permission": 20,
"provideraccount:api_list_objects": 14,
"provideraccount:list_objects_with_permission": 21,
"providernetwork:api_list_objects": 14,
"providernetwork:list_objects_with_permission": 21,
"virtualcircuit:api_list_objects": 16,
"virtualcircuit:list_objects_with_permission": 24,
"virtualcircuittermination:api_list_objects": 17,
"virtualcircuittermination:list_objects_with_permission": 23,
"virtualcircuittype:api_list_objects": 13,
"virtualcircuittype:list_objects_with_permission": 20
"circuitgroupassignment:list_objects_with_permission": 23,
"circuittermination:api_list_objects": 17,
"circuittermination:list_objects_with_permission": 21,
"circuittype:api_list_objects": 12,
"circuittype:list_objects_with_permission": 17,
"provider:api_list_objects": 14,
"provider:list_objects_with_permission": 17,
"provideraccount:api_list_objects": 13,
"provideraccount:list_objects_with_permission": 18,
"providernetwork:api_list_objects": 13,
"providernetwork:list_objects_with_permission": 18,
"virtualcircuit:api_list_objects": 15,
"virtualcircuit:list_objects_with_permission": 21,
"virtualcircuittermination:api_list_objects": 16,
"virtualcircuittermination:list_objects_with_permission": 20,
"virtualcircuittype:api_list_objects": 12,
"virtualcircuittype:list_objects_with_permission": 17
}

View File

@ -1,8 +1,8 @@
{
"datafile:api_list_objects": 10,
"datafile:list_objects_with_permission": 18,
"datasource:api_list_objects": 12,
"datasource:list_objects_with_permission": 20,
"datafile:list_objects_with_permission": 17,
"datasource:api_list_objects": 11,
"datasource:list_objects_with_permission": 17,
"job:api_list_objects": 12,
"job:list_objects_with_permission": 19
}

View File

@ -1,86 +1,86 @@
{
"cable:api_list_objects": 24,
"cable:list_objects_with_permission": 24,
"cablebundle:api_list_objects": 13,
"cablebundle:list_objects_with_permission": 20,
"cable:api_list_objects": 23,
"cable:list_objects_with_permission": 21,
"cablebundle:api_list_objects": 12,
"cablebundle:list_objects_with_permission": 17,
"cabletermination:api_list_objects": 16,
"consoleconnection:list_objects_with_permission": 29,
"consoleport:api_list_objects": 14,
"consoleport:list_objects_with_permission": 21,
"consoleport:api_list_objects": 13,
"consoleport:list_objects_with_permission": 18,
"consoleporttemplate:api_list_objects": 11,
"consoleserverport:api_list_objects": 14,
"consoleserverport:list_objects_with_permission": 21,
"consoleserverport:api_list_objects": 13,
"consoleserverport:list_objects_with_permission": 18,
"consoleserverporttemplate:api_list_objects": 11,
"device:api_list_objects": 20,
"device:list_objects_with_permission": 25,
"devicebay:api_list_objects": 14,
"devicebay:list_objects_with_permission": 21,
"device:api_list_objects": 19,
"device:list_objects_with_permission": 22,
"devicebay:api_list_objects": 13,
"devicebay:list_objects_with_permission": 18,
"devicebaytemplate:api_list_objects": 11,
"devicerole:api_list_objects": 13,
"devicerole:list_objects_with_permission": 20,
"devicetype:api_list_objects": 14,
"devicetype:list_objects_with_permission": 21,
"frontport:api_list_objects": 15,
"frontport:list_objects_with_permission": 25,
"devicerole:api_list_objects": 12,
"devicerole:list_objects_with_permission": 17,
"devicetype:api_list_objects": 13,
"devicetype:list_objects_with_permission": 18,
"frontport:api_list_objects": 14,
"frontport:list_objects_with_permission": 22,
"frontporttemplate:api_list_objects": 12,
"interface:api_list_objects": 23,
"interface:list_objects_with_permission": 21,
"interface:api_list_objects": 22,
"interface:list_objects_with_permission": 18,
"interfaceconnection:list_objects_with_permission": 41,
"interfacetemplate:api_list_objects": 11,
"inventoryitem:api_list_objects": 20,
"inventoryitem:list_objects_with_permission": 23,
"inventoryitemrole:api_list_objects": 13,
"inventoryitemrole:list_objects_with_permission": 20,
"inventoryitem:api_list_objects": 19,
"inventoryitem:list_objects_with_permission": 20,
"inventoryitemrole:api_list_objects": 12,
"inventoryitemrole:list_objects_with_permission": 17,
"inventoryitemtemplate:api_list_objects": 13,
"location:api_list_objects": 15,
"location:list_objects_with_permission": 22,
"macaddress:api_list_objects": 17,
"macaddress:list_objects_with_permission": 24,
"manufacturer:api_list_objects": 13,
"manufacturer:list_objects_with_permission": 20,
"module:api_list_objects": 18,
"module:list_objects_with_permission": 24,
"modulebay:api_list_objects": 15,
"modulebay:list_objects_with_permission": 21,
"location:api_list_objects": 14,
"location:list_objects_with_permission": 19,
"macaddress:api_list_objects": 16,
"macaddress:list_objects_with_permission": 21,
"manufacturer:api_list_objects": 12,
"manufacturer:list_objects_with_permission": 17,
"module:api_list_objects": 17,
"module:list_objects_with_permission": 21,
"modulebay:api_list_objects": 14,
"modulebay:list_objects_with_permission": 18,
"modulebaytemplate:api_list_objects": 11,
"moduletype:api_list_objects": 14,
"moduletype:list_objects_with_permission": 22,
"moduletypeprofile:api_list_objects": 13,
"moduletypeprofile:list_objects_with_permission": 20,
"platform:api_list_objects": 13,
"platform:list_objects_with_permission": 21,
"moduletype:api_list_objects": 13,
"moduletype:list_objects_with_permission": 19,
"moduletypeprofile:api_list_objects": 12,
"moduletypeprofile:list_objects_with_permission": 17,
"platform:api_list_objects": 12,
"platform:list_objects_with_permission": 18,
"powerconnection:list_objects_with_permission": 29,
"powerfeed:api_list_objects": 15,
"powerfeed:list_objects_with_permission": 22,
"poweroutlet:api_list_objects": 14,
"poweroutlet:list_objects_with_permission": 22,
"powerfeed:api_list_objects": 14,
"powerfeed:list_objects_with_permission": 19,
"poweroutlet:api_list_objects": 13,
"poweroutlet:list_objects_with_permission": 19,
"poweroutlettemplate:api_list_objects": 11,
"powerpanel:api_list_objects": 15,
"powerpanel:list_objects_with_permission": 22,
"powerport:api_list_objects": 14,
"powerport:list_objects_with_permission": 21,
"powerpanel:api_list_objects": 14,
"powerpanel:list_objects_with_permission": 19,
"powerport:api_list_objects": 13,
"powerport:list_objects_with_permission": 18,
"powerporttemplate:api_list_objects": 11,
"rack:api_list_objects": 17,
"rack:list_objects_with_permission": 29,
"rackgroup:api_list_objects": 13,
"rackgroup:list_objects_with_permission": 20,
"rackreservation:api_list_objects": 15,
"rackreservation:list_objects_with_permission": 23,
"rackrole:api_list_objects": 13,
"rackrole:list_objects_with_permission": 20,
"racktype:api_list_objects": 14,
"racktype:list_objects_with_permission": 21,
"rearport:api_list_objects": 15,
"rearport:list_objects_with_permission": 22,
"rack:api_list_objects": 16,
"rack:list_objects_with_permission": 26,
"rackgroup:api_list_objects": 12,
"rackgroup:list_objects_with_permission": 17,
"rackreservation:api_list_objects": 14,
"rackreservation:list_objects_with_permission": 20,
"rackrole:api_list_objects": 12,
"rackrole:list_objects_with_permission": 17,
"racktype:api_list_objects": 13,
"racktype:list_objects_with_permission": 18,
"rearport:api_list_objects": 14,
"rearport:list_objects_with_permission": 19,
"rearporttemplate:api_list_objects": 12,
"region:api_list_objects": 13,
"region:list_objects_with_permission": 20,
"site:api_list_objects": 17,
"site:list_objects_with_permission": 22,
"sitegroup:api_list_objects": 13,
"sitegroup:list_objects_with_permission": 20,
"virtualchassis:api_list_objects": 16,
"virtualchassis:list_objects_with_permission": 21,
"virtualdevicecontext:api_list_objects": 14,
"virtualdevicecontext:list_objects_with_permission": 20
"region:api_list_objects": 12,
"region:list_objects_with_permission": 17,
"site:api_list_objects": 16,
"site:list_objects_with_permission": 19,
"sitegroup:api_list_objects": 12,
"sitegroup:list_objects_with_permission": 17,
"virtualchassis:api_list_objects": 15,
"virtualchassis:list_objects_with_permission": 18,
"virtualdevicecontext:api_list_objects": 13,
"virtualdevicecontext:list_objects_with_permission": 17
}

View File

@ -71,10 +71,12 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
"""
Return all CustomFields assigned to the given model.
"""
# Check the request cache before hitting the database
# Check the request cache before hitting the database. Test the cached value against None
# rather than for truthiness: a model with no custom fields caches an empty QuerySet, which
# would otherwise be treated as a miss and re-queried on every call.
cache = query_cache.get()
if cache is not None:
if custom_fields := cache['custom_fields'].get(model._meta.model):
if (custom_fields := cache['custom_fields'].get(model._meta.model)) is not None:
return custom_fields
content_type = ObjectType.objects.get_for_model(model._meta.concrete_model)

View File

@ -4,7 +4,7 @@
"configcontext:api_list_objects": 22,
"configcontext:list_objects_with_permission": 16,
"configcontextprofile:api_list_objects": 12,
"configcontextprofile:list_objects_with_permission": 19,
"configcontextprofile:list_objects_with_permission": 16,
"configtemplate:api_list_objects": 10,
"configtemplate:list_objects_with_permission": 17,
"customfield:api_list_objects": 10,
@ -13,14 +13,14 @@
"customfieldchoiceset:list_objects_with_permission": 16,
"customlink:api_list_objects": 10,
"customlink:list_objects_with_permission": 18,
"eventrule:api_list_objects": 16,
"eventrule:list_objects_with_permission": 23,
"eventrule:api_list_objects": 15,
"eventrule:list_objects_with_permission": 20,
"exporttemplate:api_list_objects": 10,
"exporttemplate:list_objects_with_permission": 19,
"imageattachment:api_list_objects": 11,
"imageattachment:list_objects_with_permission": 21,
"journalentry:api_list_objects": 16,
"journalentry:list_objects_with_permission": 24,
"journalentry:api_list_objects": 15,
"journalentry:list_objects_with_permission": 21,
"notification:api_list_objects": 12,
"notificationgroup:api_list_objects": 11,
"notificationgroup:list_objects_with_permission": 18,
@ -32,6 +32,6 @@
"tag:api_list_objects": 10,
"tag:list_objects_with_permission": 18,
"taggeditem:api_list_objects": 12,
"webhook:api_list_objects": 13,
"webhook:list_objects_with_permission": 20
"webhook:api_list_objects": 12,
"webhook:list_objects_with_permission": 17
}

View File

@ -1,5 +1,6 @@
import datetime
import json
from collections import defaultdict
from decimal import Decimal
from unittest.mock import patch
@ -16,6 +17,7 @@ from extras.choices import *
from extras.models import CustomField, CustomFieldChoiceSet
from ipam.models import VLAN
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
from netbox.context import query_cache
from utilities.testing import APITestCase, TestCase
from virtualization.models import VirtualMachine
@ -826,6 +828,22 @@ class CustomFieldManagerTestCase(TestCase):
self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1)
self.assertEqual(CustomField.objects.get_for_model(VirtualMachine).count(), 0)
def test_get_for_model_caches_models_with_no_custom_fields(self):
"""
A model with no custom fields assigned must be served from the request cache like any other.
An empty QuerySet is falsy, so testing the cached value for truthiness would treat it as a
miss and re-query on every call.
"""
token = query_cache.set(defaultdict(dict))
self.addCleanup(query_cache.reset, token)
# Site has one custom field assigned, VirtualMachine none
for model in (Site, VirtualMachine):
# Prime the cache, iterating so that the QuerySet's own result cache is populated too
list(CustomField.objects.get_for_model(model))
with self.assertNumQueries(0):
list(CustomField.objects.get_for_model(model))
class CustomFieldAPITestCase(APITestCase):

View File

@ -1,37 +1,37 @@
{
"aggregate:api_list_objects": 14,
"aggregate:list_objects_with_permission": 24,
"asn:api_list_objects": 18,
"asn:list_objects_with_permission": 31,
"asnrange:api_list_objects": 15,
"asnrange:list_objects_with_permission": 22,
"fhrpgroup:api_list_objects": 14,
"fhrpgroup:list_objects_with_permission": 21,
"aggregate:api_list_objects": 13,
"aggregate:list_objects_with_permission": 21,
"asn:api_list_objects": 17,
"asn:list_objects_with_permission": 28,
"asnrange:api_list_objects": 14,
"asnrange:list_objects_with_permission": 19,
"fhrpgroup:api_list_objects": 13,
"fhrpgroup:list_objects_with_permission": 18,
"fhrpgroupassignment:api_list_objects": 18,
"ipaddress:api_list_objects": 14,
"ipaddress:list_objects_with_permission": 21,
"iprange:api_list_objects": 13,
"iprange:list_objects_with_permission": 20,
"prefix:api_list_objects": 13,
"prefix:list_objects_with_permission": 29,
"rir:api_list_objects": 13,
"rir:list_objects_with_permission": 20,
"role:api_list_objects": 13,
"role:list_objects_with_permission": 20,
"routetarget:api_list_objects": 13,
"routetarget:list_objects_with_permission": 21,
"service:api_list_objects": 16,
"service:list_objects_with_permission": 21,
"servicetemplate:api_list_objects": 13,
"servicetemplate:list_objects_with_permission": 20,
"vlan:api_list_objects": 15,
"vlan:list_objects_with_permission": 24,
"vlangroup:api_list_objects": 13,
"vlangroup:list_objects_with_permission": 25,
"ipaddress:api_list_objects": 13,
"ipaddress:list_objects_with_permission": 18,
"iprange:api_list_objects": 12,
"iprange:list_objects_with_permission": 17,
"prefix:api_list_objects": 12,
"prefix:list_objects_with_permission": 26,
"rir:api_list_objects": 12,
"rir:list_objects_with_permission": 17,
"role:api_list_objects": 12,
"role:list_objects_with_permission": 17,
"routetarget:api_list_objects": 12,
"routetarget:list_objects_with_permission": 18,
"service:api_list_objects": 15,
"service:list_objects_with_permission": 18,
"servicetemplate:api_list_objects": 12,
"servicetemplate:list_objects_with_permission": 17,
"vlan:api_list_objects": 14,
"vlan:list_objects_with_permission": 21,
"vlangroup:api_list_objects": 12,
"vlangroup:list_objects_with_permission": 22,
"vlantranslationpolicy:api_list_objects": 12,
"vlantranslationpolicy:list_objects_with_permission": 20,
"vlantranslationpolicy:list_objects_with_permission": 17,
"vlantranslationrule:api_list_objects": 12,
"vlantranslationrule:list_objects_with_permission": 21,
"vrf:api_list_objects": 15,
"vrf:list_objects_with_permission": 20
"vlantranslationrule:list_objects_with_permission": 18,
"vrf:api_list_objects": 14,
"vrf:list_objects_with_permission": 17
}

View File

@ -1,14 +1,14 @@
{
"contact:api_list_objects": 14,
"contact:list_objects_with_permission": 21,
"contactassignment:api_list_objects": 17,
"contactassignment:list_objects_with_permission": 25,
"contactgroup:api_list_objects": 14,
"contactgroup:list_objects_with_permission": 20,
"contactrole:api_list_objects": 13,
"contactrole:list_objects_with_permission": 20,
"tenant:api_list_objects": 14,
"tenant:list_objects_with_permission": 21,
"tenantgroup:api_list_objects": 14,
"tenantgroup:list_objects_with_permission": 20
"contact:api_list_objects": 13,
"contact:list_objects_with_permission": 18,
"contactassignment:api_list_objects": 16,
"contactassignment:list_objects_with_permission": 22,
"contactgroup:api_list_objects": 13,
"contactgroup:list_objects_with_permission": 17,
"contactrole:api_list_objects": 12,
"contactrole:list_objects_with_permission": 17,
"tenant:api_list_objects": 13,
"tenant:list_objects_with_permission": 18,
"tenantgroup:api_list_objects": 13,
"tenantgroup:list_objects_with_permission": 17
}

View File

@ -1,14 +1,14 @@
{
"group:api_list_objects": 10,
"group:list_objects_with_permission": 17,
"group:list_objects_with_permission": 16,
"objectpermission:api_list_objects": 14,
"objectpermission:list_objects_with_permission": 18,
"objectpermission:list_objects_with_permission": 17,
"owner:api_list_objects": 11,
"owner:list_objects_with_permission": 19,
"owner:list_objects_with_permission": 18,
"ownergroup:api_list_objects": 9,
"ownergroup:list_objects_with_permission": 17,
"ownergroup:list_objects_with_permission": 16,
"token:api_list_objects": 10,
"token:list_objects_with_permission": 17,
"user:api_list_objects": 12,
"user:list_objects_with_permission": 17
"user:list_objects_with_permission": 16
}

View File

@ -1,16 +1,16 @@
{
"cluster:api_list_objects": 16,
"cluster:list_objects_with_permission": 22,
"clustergroup:api_list_objects": 13,
"clustergroup:list_objects_with_permission": 20,
"clustertype:api_list_objects": 13,
"clustertype:list_objects_with_permission": 20,
"virtualdisk:api_list_objects": 14,
"virtualdisk:list_objects_with_permission": 21,
"virtualmachine:api_list_objects": 18,
"virtualmachine:list_objects_with_permission": 24,
"virtualmachinetype:api_list_objects": 14,
"virtualmachinetype:list_objects_with_permission": 21,
"vminterface:api_list_objects": 19,
"vminterface:list_objects_with_permission": 21
"cluster:api_list_objects": 15,
"cluster:list_objects_with_permission": 19,
"clustergroup:api_list_objects": 12,
"clustergroup:list_objects_with_permission": 17,
"clustertype:api_list_objects": 12,
"clustertype:list_objects_with_permission": 17,
"virtualdisk:api_list_objects": 13,
"virtualdisk:list_objects_with_permission": 18,
"virtualmachine:api_list_objects": 17,
"virtualmachine:list_objects_with_permission": 21,
"virtualmachinetype:api_list_objects": 13,
"virtualmachinetype:list_objects_with_permission": 18,
"vminterface:api_list_objects": 18,
"vminterface:list_objects_with_permission": 18
}

View File

@ -1,22 +1,22 @@
{
"ikepolicy:api_list_objects": 14,
"ikepolicy:list_objects_with_permission": 21,
"ikeproposal:api_list_objects": 13,
"ikeproposal:list_objects_with_permission": 20,
"ipsecpolicy:api_list_objects": 14,
"ipsecpolicy:list_objects_with_permission": 21,
"ipsecprofile:api_list_objects": 15,
"ipsecprofile:list_objects_with_permission": 22,
"ipsecproposal:api_list_objects": 13,
"ipsecproposal:list_objects_with_permission": 20,
"l2vpn:api_list_objects": 15,
"l2vpn:list_objects_with_permission": 20,
"l2vpntermination:api_list_objects": 16,
"l2vpntermination:list_objects_with_permission": 25,
"tunnel:api_list_objects": 14,
"tunnel:list_objects_with_permission": 21,
"tunnelgroup:api_list_objects": 13,
"tunnelgroup:list_objects_with_permission": 20,
"tunneltermination:api_list_objects": 18,
"tunneltermination:list_objects_with_permission": 28
"ikepolicy:api_list_objects": 13,
"ikepolicy:list_objects_with_permission": 18,
"ikeproposal:api_list_objects": 12,
"ikeproposal:list_objects_with_permission": 17,
"ipsecpolicy:api_list_objects": 13,
"ipsecpolicy:list_objects_with_permission": 18,
"ipsecprofile:api_list_objects": 14,
"ipsecprofile:list_objects_with_permission": 19,
"ipsecproposal:api_list_objects": 12,
"ipsecproposal:list_objects_with_permission": 17,
"l2vpn:api_list_objects": 14,
"l2vpn:list_objects_with_permission": 17,
"l2vpntermination:api_list_objects": 15,
"l2vpntermination:list_objects_with_permission": 22,
"tunnel:api_list_objects": 13,
"tunnel:list_objects_with_permission": 18,
"tunnelgroup:api_list_objects": 12,
"tunnelgroup:list_objects_with_permission": 17,
"tunneltermination:api_list_objects": 17,
"tunneltermination:list_objects_with_permission": 25
}

View File

@ -1,8 +1,8 @@
{
"wirelesslan:api_list_objects": 13,
"wirelesslan:list_objects_with_permission": 21,
"wirelesslangroup:api_list_objects": 13,
"wirelesslangroup:list_objects_with_permission": 21,
"wirelesslink:api_list_objects": 18,
"wirelesslink:list_objects_with_permission": 24
"wirelesslan:api_list_objects": 12,
"wirelesslan:list_objects_with_permission": 18,
"wirelesslangroup:api_list_objects": 12,
"wirelesslangroup:list_objects_with_permission": 18,
"wirelesslink:api_list_objects": 17,
"wirelesslink:list_objects_with_permission": 21
}