Explorar o código

fixes #3428 - caching invalidation issues

Mitgate invalidation issues by using prefetch_related instead of select_related.
Also use invalidated_update instead of just update.
John Anderson %!s(int64=6) %!d(string=hai) anos
pai
achega
ade844f7a7

+ 1 - 1
docs/additional-features/reports.md

@@ -43,7 +43,7 @@ class DeviceConnectionsReport(Report):
     def test_console_connection(self):
 
         # Check that every console port for every active device has a connection defined.
-        for console_port in ConsolePort.objects.select_related('device').filter(device__status=DEVICE_STATUS_ACTIVE):
+        for console_port in ConsolePort.objects.prefetch_related('device').filter(device__status=DEVICE_STATUS_ACTIVE):
             if console_port.connected_endpoint is None:
                 self.log_failure(
                     console_port.device,

+ 1 - 1
docs/development/extending-models.md

@@ -38,7 +38,7 @@ Add the name of the new field to `csv_headers` and included a CSV-friendly repre
 
 ### 4. Update relevant querysets
 
-If you're adding a relational field (e.g. `ForeignKey`) and intend to include the data when retreiving a list of objects, be sure to include the field using `select_related()` or `prefetch_related()` as appropriate. This will optimize the view and avoid excessive database lookups.
+If you're adding a relational field (e.g. `ForeignKey`) and intend to include the data when retreiving a list of objects, be sure to include the field using `prefetch_related()` as appropriate. This will optimize the view and avoid excessive database lookups.
 
 ### 5. Update API serializer
 

+ 2 - 2
netbox/circuits/api/views.py

@@ -62,7 +62,7 @@ class CircuitTypeViewSet(ModelViewSet):
 #
 
 class CircuitViewSet(CustomFieldModelViewSet):
-    queryset = Circuit.objects.select_related('type', 'tenant', 'provider').prefetch_related('tags')
+    queryset = Circuit.objects.prefetch_related('type', 'tenant', 'provider').prefetch_related('tags')
     serializer_class = serializers.CircuitSerializer
     filterset_class = filters.CircuitFilter
 
@@ -72,7 +72,7 @@ class CircuitViewSet(CustomFieldModelViewSet):
 #
 
 class CircuitTerminationViewSet(ModelViewSet):
-    queryset = CircuitTermination.objects.select_related(
+    queryset = CircuitTermination.objects.prefetch_related(
         'circuit', 'site', 'connected_endpoint__device', 'cable'
     )
     serializer_class = serializers.CircuitTerminationSerializer

+ 1 - 1
netbox/circuits/models.py

@@ -295,6 +295,6 @@ class CircuitTermination(CableTermination):
     def get_peer_termination(self):
         peer_side = 'Z' if self.term_side == 'A' else 'A'
         try:
-            return CircuitTermination.objects.select_related('site').get(circuit=self.circuit, term_side=peer_side)
+            return CircuitTermination.objects.prefetch_related('site').get(circuit=self.circuit, term_side=peer_side)
         except CircuitTermination.DoesNotExist:
             return None

+ 1 - 1
netbox/circuits/signals.py

@@ -10,4 +10,4 @@ def update_circuit(instance, **kwargs):
     """
     When a CircuitTermination has been modified, update the last_updated time of its parent Circuit.
     """
-    Circuit.objects.filter(pk=instance.circuit_id).update(last_updated=timezone.now())
+    Circuit.objects.filter(pk=instance.circuit_id).invalidated_update(last_updated=timezone.now())

+ 8 - 14
netbox/circuits/views.py

@@ -35,11 +35,7 @@ class ProviderView(PermissionRequiredMixin, View):
     def get(self, request, slug):
 
         provider = get_object_or_404(Provider, slug=slug)
-        circuits = Circuit.objects.filter(provider=provider).select_related(
-            'type', 'tenant'
-        ).prefetch_related(
-            'terminations__site'
-        )
+        circuits = Circuit.objects.filter(provider=provider).prefetch_related('type', 'tenant', 'terminations__site')
         show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
 
         return render(request, 'circuits/provider.html', {
@@ -134,10 +130,8 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class CircuitListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'circuits.view_circuit'
     _terminations = CircuitTermination.objects.filter(circuit=OuterRef('pk'))
-    queryset = Circuit.objects.select_related(
-        'provider', 'type', 'tenant'
-    ).prefetch_related(
-        'terminations__site'
+    queryset = Circuit.objects.prefetch_related(
+        'provider', 'type', 'tenant', 'terminations__site'
     ).annotate(
         a_side=Subquery(_terminations.filter(term_side='A').values('site__name')[:1]),
         z_side=Subquery(_terminations.filter(term_side='Z').values('site__name')[:1]),
@@ -153,13 +147,13 @@ class CircuitView(PermissionRequiredMixin, View):
 
     def get(self, request, pk):
 
-        circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
-        termination_a = CircuitTermination.objects.select_related(
+        circuit = get_object_or_404(Circuit.objects.prefetch_related('provider', 'type', 'tenant__group'), pk=pk)
+        termination_a = CircuitTermination.objects.prefetch_related(
             'site__region', 'connected_endpoint__device'
         ).filter(
             circuit=circuit, term_side=TERM_SIDE_A
         ).first()
-        termination_z = CircuitTermination.objects.select_related(
+        termination_z = CircuitTermination.objects.prefetch_related(
             'site__region', 'connected_endpoint__device'
         ).filter(
             circuit=circuit, term_side=TERM_SIDE_Z
@@ -199,7 +193,7 @@ class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'circuits.change_circuit'
-    queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
+    queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
     filter = filters.CircuitFilter
     table = tables.CircuitTable
     form = forms.CircuitBulkEditForm
@@ -208,7 +202,7 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'circuits.delete_circuit'
-    queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
+    queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
     filter = filters.CircuitFilter
     table = tables.CircuitTable
     default_return_url = 'circuits:circuit_list'

+ 34 - 68
netbox/dcim/api/views.py

@@ -109,10 +109,8 @@ class RegionViewSet(ModelViewSet):
 #
 
 class SiteViewSet(CustomFieldModelViewSet):
-    queryset = Site.objects.select_related(
-        'region', 'tenant'
-    ).prefetch_related(
-        'tags'
+    queryset = Site.objects.prefetch_related(
+        'region', 'tenant', 'tags'
     ).annotate(
         device_count=get_subquery(Device, 'site'),
         rack_count=get_subquery(Rack, 'site'),
@@ -140,7 +138,7 @@ class SiteViewSet(CustomFieldModelViewSet):
 #
 
 class RackGroupViewSet(ModelViewSet):
-    queryset = RackGroup.objects.select_related('site').annotate(
+    queryset = RackGroup.objects.prefetch_related('site').annotate(
         rack_count=Count('racks')
     )
     serializer_class = serializers.RackGroupSerializer
@@ -164,10 +162,8 @@ class RackRoleViewSet(ModelViewSet):
 #
 
 class RackViewSet(CustomFieldModelViewSet):
-    queryset = Rack.objects.select_related(
-        'site', 'group__site', 'role', 'tenant'
-    ).prefetch_related(
-        'tags'
+    queryset = Rack.objects.prefetch_related(
+        'site', 'group__site', 'role', 'tenant', 'tags'
     ).annotate(
         device_count=get_subquery(Device, 'rack'),
         powerfeed_count=get_subquery(PowerFeed, 'rack')
@@ -206,7 +202,7 @@ class RackViewSet(CustomFieldModelViewSet):
 #
 
 class RackReservationViewSet(ModelViewSet):
-    queryset = RackReservation.objects.select_related('rack', 'user', 'tenant')
+    queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
     serializer_class = serializers.RackReservationSerializer
     filterset_class = filters.RackReservationFilter
 
@@ -234,7 +230,7 @@ class ManufacturerViewSet(ModelViewSet):
 #
 
 class DeviceTypeViewSet(CustomFieldModelViewSet):
-    queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('tags').annotate(
+    queryset = DeviceType.objects.prefetch_related('manufacturer').prefetch_related('tags').annotate(
         device_count=Count('instances')
     )
     serializer_class = serializers.DeviceTypeSerializer
@@ -246,49 +242,49 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
 #
 
 class ConsolePortTemplateViewSet(ModelViewSet):
-    queryset = ConsolePortTemplate.objects.select_related('device_type__manufacturer')
+    queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.ConsolePortTemplateSerializer
     filterset_class = filters.ConsolePortTemplateFilter
 
 
 class ConsoleServerPortTemplateViewSet(ModelViewSet):
-    queryset = ConsoleServerPortTemplate.objects.select_related('device_type__manufacturer')
+    queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.ConsoleServerPortTemplateSerializer
     filterset_class = filters.ConsoleServerPortTemplateFilter
 
 
 class PowerPortTemplateViewSet(ModelViewSet):
-    queryset = PowerPortTemplate.objects.select_related('device_type__manufacturer')
+    queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.PowerPortTemplateSerializer
     filterset_class = filters.PowerPortTemplateFilter
 
 
 class PowerOutletTemplateViewSet(ModelViewSet):
-    queryset = PowerOutletTemplate.objects.select_related('device_type__manufacturer')
+    queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.PowerOutletTemplateSerializer
     filterset_class = filters.PowerOutletTemplateFilter
 
 
 class InterfaceTemplateViewSet(ModelViewSet):
-    queryset = InterfaceTemplate.objects.select_related('device_type__manufacturer')
+    queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.InterfaceTemplateSerializer
     filterset_class = filters.InterfaceTemplateFilter
 
 
 class FrontPortTemplateViewSet(ModelViewSet):
-    queryset = FrontPortTemplate.objects.select_related('device_type__manufacturer')
+    queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.FrontPortTemplateSerializer
     filterset_class = filters.FrontPortTemplateFilter
 
 
 class RearPortTemplateViewSet(ModelViewSet):
-    queryset = RearPortTemplate.objects.select_related('device_type__manufacturer')
+    queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.RearPortTemplateSerializer
     filterset_class = filters.RearPortTemplateFilter
 
 
 class DeviceBayTemplateViewSet(ModelViewSet):
-    queryset = DeviceBayTemplate.objects.select_related('device_type__manufacturer')
+    queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.DeviceBayTemplateSerializer
     filterset_class = filters.DeviceBayTemplateFilter
 
@@ -324,11 +320,9 @@ class PlatformViewSet(ModelViewSet):
 #
 
 class DeviceViewSet(CustomFieldModelViewSet):
-    queryset = Device.objects.select_related(
+    queryset = Device.objects.prefetch_related(
         'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
-        'virtual_chassis__master',
-    ).prefetch_related(
-        'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
+        'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
     )
     filterset_class = filters.DeviceFilter
 
@@ -429,52 +423,36 @@ class DeviceViewSet(CustomFieldModelViewSet):
 #
 
 class ConsolePortViewSet(CableTraceMixin, ModelViewSet):
-    queryset = ConsolePort.objects.select_related(
-        'device', 'connected_endpoint__device', 'cable'
-    ).prefetch_related(
-        'tags'
-    )
+    queryset = ConsolePort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
     serializer_class = serializers.ConsolePortSerializer
     filterset_class = filters.ConsolePortFilter
 
 
 class ConsoleServerPortViewSet(CableTraceMixin, ModelViewSet):
-    queryset = ConsoleServerPort.objects.select_related(
-        'device', 'connected_endpoint__device', 'cable'
-    ).prefetch_related(
-        'tags'
-    )
+    queryset = ConsoleServerPort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
     serializer_class = serializers.ConsoleServerPortSerializer
     filterset_class = filters.ConsoleServerPortFilter
 
 
 class PowerPortViewSet(CableTraceMixin, ModelViewSet):
-    queryset = PowerPort.objects.select_related(
-        'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable'
-    ).prefetch_related(
-        'tags'
+    queryset = PowerPort.objects.prefetch_related(
+        'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable', 'tags'
     )
     serializer_class = serializers.PowerPortSerializer
     filterset_class = filters.PowerPortFilter
 
 
 class PowerOutletViewSet(CableTraceMixin, ModelViewSet):
-    queryset = PowerOutlet.objects.select_related(
-        'device', 'connected_endpoint__device', 'cable'
-    ).prefetch_related(
-        'tags'
-    )
+    queryset = PowerOutlet.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
     serializer_class = serializers.PowerOutletSerializer
     filterset_class = filters.PowerOutletFilter
 
 
 class InterfaceViewSet(CableTraceMixin, ModelViewSet):
-    queryset = Interface.objects.filter(
+    queryset = Interface.objects.prefetch_related(
+        'device', '_connected_interface', '_connected_circuittermination', 'cable', 'ip_addresses', 'tags'
+    ).filter(
         device__isnull=False
-    ).select_related(
-        'device', '_connected_interface', '_connected_circuittermination', 'cable'
-    ).prefetch_related(
-        'ip_addresses', 'tags'
     )
     serializer_class = serializers.InterfaceSerializer
     filterset_class = filters.InterfaceFilter
@@ -491,33 +469,25 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
 
 
 class FrontPortViewSet(ModelViewSet):
-    queryset = FrontPort.objects.select_related(
-        'device__device_type__manufacturer', 'rear_port', 'cable'
-    ).prefetch_related(
-        'tags'
-    )
+    queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')
     serializer_class = serializers.FrontPortSerializer
     filterset_class = filters.FrontPortFilter
 
 
 class RearPortViewSet(ModelViewSet):
-    queryset = RearPort.objects.select_related(
-        'device__device_type__manufacturer', 'cable'
-    ).prefetch_related(
-        'tags'
-    )
+    queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags')
     serializer_class = serializers.RearPortSerializer
     filterset_class = filters.RearPortFilter
 
 
 class DeviceBayViewSet(ModelViewSet):
-    queryset = DeviceBay.objects.select_related('installed_device').prefetch_related('tags')
+    queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags')
     serializer_class = serializers.DeviceBaySerializer
     filterset_class = filters.DeviceBayFilter
 
 
 class InventoryItemViewSet(ModelViewSet):
-    queryset = InventoryItem.objects.select_related('device', 'manufacturer').prefetch_related('tags')
+    queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags')
     serializer_class = serializers.InventoryItemSerializer
     filterset_class = filters.InventoryItemFilter
 
@@ -527,7 +497,7 @@ class InventoryItemViewSet(ModelViewSet):
 #
 
 class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
-    queryset = ConsolePort.objects.select_related(
+    queryset = ConsolePort.objects.prefetch_related(
         'device', 'connected_endpoint__device'
     ).filter(
         connected_endpoint__isnull=False
@@ -537,7 +507,7 @@ class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
 
 
 class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
-    queryset = PowerPort.objects.select_related(
+    queryset = PowerPort.objects.prefetch_related(
         'device', 'connected_endpoint__device'
     ).filter(
         _connected_poweroutlet__isnull=False
@@ -547,7 +517,7 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
 
 
 class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
-    queryset = Interface.objects.select_related(
+    queryset = Interface.objects.prefetch_related(
         'device', '_connected_interface__device'
     ).filter(
         # Avoid duplicate connections by only selecting the lower PK in a connected pair
@@ -587,7 +557,7 @@ class VirtualChassisViewSet(ModelViewSet):
 #
 
 class PowerPanelViewSet(ModelViewSet):
-    queryset = PowerPanel.objects.select_related(
+    queryset = PowerPanel.objects.prefetch_related(
         'site', 'rack_group'
     ).annotate(
         powerfeed_count=Count('powerfeeds')
@@ -601,11 +571,7 @@ class PowerPanelViewSet(ModelViewSet):
 #
 
 class PowerFeedViewSet(CustomFieldModelViewSet):
-    queryset = PowerFeed.objects.select_related(
-        'power_panel', 'rack'
-    ).prefetch_related(
-        'tags'
-    )
+    queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack', 'tags')
     serializer_class = serializers.PowerFeedSerializer
     filterset_class = filters.PowerFeedFilter
 

+ 6 - 6
netbox/dcim/forms.py

@@ -632,7 +632,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
     )
     group_id = ChainedModelChoiceField(
         label='Rack group',
-        queryset=RackGroup.objects.select_related('site'),
+        queryset=RackGroup.objects.prefetch_related('site'),
         chains=(
             ('site', 'site'),
         ),
@@ -745,7 +745,7 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
         )
     )
     group_id = FilterChoiceField(
-        queryset=RackGroup.objects.select_related('site'),
+        queryset=RackGroup.objects.prefetch_related('site'),
         label='Rack group',
         null_label='-- None --',
         widget=APISelectMultiple(
@@ -1391,14 +1391,14 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
                 interface_ids = self.instance.vc_interfaces.values('pk')
 
                 # Collect interface IPs
-                interface_ips = IPAddress.objects.select_related('interface').filter(
+                interface_ips = IPAddress.objects.prefetch_related('interface').filter(
                     family=family, interface_id__in=interface_ids
                 )
                 if interface_ips:
                     ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
                     ip_choices.append(('Interface IPs', ip_list))
                 # Collect NAT IPs
-                nat_ips = IPAddress.objects.select_related('nat_inside').filter(
+                nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
                     family=family, nat_inside__interface__in=interface_ids
                 )
                 if nat_ips:
@@ -1710,7 +1710,7 @@ class DeviceFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
         )
     )
     rack_group_id = FilterChoiceField(
-        queryset=RackGroup.objects.select_related(
+        queryset=RackGroup.objects.prefetch_related(
             'site'
         ),
         label='Rack group',
@@ -1749,7 +1749,7 @@ class DeviceFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
         )
     )
     device_type_id = FilterChoiceField(
-        queryset=DeviceType.objects.select_related(
+        queryset=DeviceType.objects.prefetch_related(
             'manufacturer'
         ),
         label='Model',

+ 4 - 4
netbox/dcim/models.py

@@ -607,7 +607,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
 
         # Update racked devices if the assigned Site has been changed.
         if _site_id is not None and self.site_id != _site_id:
-            Device.objects.filter(rack=self).update(site_id=self.site.pk)
+            Device.objects.filter(rack=self).invalidated_update(site_id=self.site.pk)
 
     def to_csv(self):
         return (
@@ -664,7 +664,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
 
         # Add devices to rack units list
         if self.pk:
-            for device in Device.objects.select_related('device_type__manufacturer', 'device_role')\
+            for device in Device.objects.prefetch_related('device_type__manufacturer', 'device_role')\
                     .annotate(devicebay_count=Count('device_bays'))\
                     .exclude(pk=exclude)\
                     .filter(rack=self, position__gt=0)\
@@ -697,7 +697,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
         """
 
         # Gather all devices which consume U space within the rack
-        devices = self.devices.select_related('device_type').filter(position__gte=1).exclude(pk__in=exclude)
+        devices = self.devices.prefetch_related('device_type').filter(position__gte=1).exclude(pk__in=exclude)
 
         # Initialize the rack unit skeleton
         units = list(range(1, self.u_height + 1))
@@ -1738,7 +1738,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
             )
 
         # Update Site and Rack assignment for any child Devices
-        Device.objects.filter(parent_bay__device=self).update(site=self.site, rack=self.rack)
+        Device.objects.filter(parent_bay__device=self).invalidated_update(site=self.site, rack=self.rack)
 
     def to_csv(self):
         return (

+ 2 - 2
netbox/dcim/signals.py

@@ -10,7 +10,7 @@ def assign_virtualchassis_master(instance, created, **kwargs):
     When a VirtualChassis is created, automatically assign its master device to the VC.
     """
     if created:
-        Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=None)
+        Device.objects.filter(pk=instance.master.pk).invalidated_update(virtual_chassis=instance, vc_position=None)
 
 
 @receiver(pre_delete, sender=VirtualChassis)
@@ -18,7 +18,7 @@ def clear_virtualchassis_members(instance, **kwargs):
     """
     When a VirtualChassis is deleted, nullify the vc_position and vc_priority fields of its prior members.
     """
-    Device.objects.filter(virtual_chassis=instance.pk).update(vc_position=None, vc_priority=None)
+    Device.objects.filter(virtual_chassis=instance.pk).invalidated_update(vc_position=None, vc_priority=None)
 
 
 @receiver(post_save, sender=Cable)

+ 4 - 4
netbox/dcim/tests/test_api.py

@@ -3430,11 +3430,11 @@ class VirtualChassisTest(APITestCase):
 
         # Create two VirtualChassis with three members each
         self.vc1 = VirtualChassis.objects.create(master=self.device1, domain='test-domain-1')
-        Device.objects.filter(pk=self.device2.pk).update(virtual_chassis=self.vc1, vc_position=2)
-        Device.objects.filter(pk=self.device3.pk).update(virtual_chassis=self.vc1, vc_position=3)
+        Device.objects.filter(pk=self.device2.pk).invalidated_update(virtual_chassis=self.vc1, vc_position=2)
+        Device.objects.filter(pk=self.device3.pk).invalidated_update(virtual_chassis=self.vc1, vc_position=3)
         self.vc2 = VirtualChassis.objects.create(master=self.device4, domain='test-domain-2')
-        Device.objects.filter(pk=self.device5.pk).update(virtual_chassis=self.vc2, vc_position=2)
-        Device.objects.filter(pk=self.device6.pk).update(virtual_chassis=self.vc2, vc_position=3)
+        Device.objects.filter(pk=self.device5.pk).invalidated_update(virtual_chassis=self.vc2, vc_position=2)
+        Device.objects.filter(pk=self.device6.pk).invalidated_update(virtual_chassis=self.vc2, vc_position=3)
 
     def test_get_virtualchassis(self):
 

+ 3 - 3
netbox/dcim/tests/test_views.py

@@ -442,11 +442,11 @@ class VirtualChassisTestCase(TestCase):
 
         # Create three VirtualChassis with two members each
         vc1 = VirtualChassis.objects.create(master=device1, domain='test-domain-1')
-        Device.objects.filter(pk=device2.pk).update(virtual_chassis=vc1, vc_position=2)
+        Device.objects.filter(pk=device2.pk).invalidated_update(virtual_chassis=vc1, vc_position=2)
         vc2 = VirtualChassis.objects.create(master=device3, domain='test-domain-2')
-        Device.objects.filter(pk=device4.pk).update(virtual_chassis=vc2, vc_position=2)
+        Device.objects.filter(pk=device4.pk).invalidated_update(virtual_chassis=vc2, vc_position=2)
         vc3 = VirtualChassis.objects.create(master=device5, domain='test-domain-3')
-        Device.objects.filter(pk=device6.pk).update(virtual_chassis=vc3, vc_position=2)
+        Device.objects.filter(pk=device6.pk).invalidated_update(virtual_chassis=vc3, vc_position=2)
 
     def test_virtualchassis_list(self):
 

+ 63 - 68
netbox/dcim/views.py

@@ -185,7 +185,7 @@ class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class SiteListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_site'
-    queryset = Site.objects.select_related('region', 'tenant')
+    queryset = Site.objects.prefetch_related('region', 'tenant')
     filter = filters.SiteFilter
     filter_form = forms.SiteFilterForm
     table = tables.SiteTable
@@ -197,7 +197,7 @@ class SiteView(PermissionRequiredMixin, View):
 
     def get(self, request, slug):
 
-        site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
+        site = get_object_or_404(Site.objects.prefetch_related('region', 'tenant__group'), slug=slug)
         stats = {
             'rack_count': Rack.objects.filter(site=site).count(),
             'device_count': Device.objects.filter(site=site).count(),
@@ -246,7 +246,7 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_site'
-    queryset = Site.objects.select_related('region', 'tenant')
+    queryset = Site.objects.prefetch_related('region', 'tenant')
     filter = filters.SiteFilter
     table = tables.SiteTable
     form = forms.SiteBulkEditForm
@@ -255,7 +255,7 @@ class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class SiteBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_site'
-    queryset = Site.objects.select_related('region', 'tenant')
+    queryset = Site.objects.prefetch_related('region', 'tenant')
     filter = filters.SiteFilter
     table = tables.SiteTable
     default_return_url = 'dcim:site_list'
@@ -267,7 +267,7 @@ class SiteBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class RackGroupListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_rackgroup'
-    queryset = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
+    queryset = RackGroup.objects.prefetch_related('site').annotate(rack_count=Count('racks'))
     filter = filters.RackGroupFilter
     filter_form = forms.RackGroupFilterForm
     table = tables.RackGroupTable
@@ -294,7 +294,7 @@ class RackGroupBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_rackgroup'
-    queryset = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
+    queryset = RackGroup.objects.prefetch_related('site').annotate(rack_count=Count('racks'))
     filter = filters.RackGroupFilter
     table = tables.RackGroupTable
     default_return_url = 'dcim:rackgroup_list'
@@ -342,10 +342,8 @@ class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class RackListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_rack'
-    queryset = Rack.objects.select_related(
-        'site', 'group', 'tenant', 'role'
-    ).prefetch_related(
-        'devices__device_type'
+    queryset = Rack.objects.prefetch_related(
+        'site', 'group', 'tenant', 'role', 'devices__device_type'
     ).annotate(
         device_count=Count('devices')
     )
@@ -363,11 +361,7 @@ class RackElevationListView(PermissionRequiredMixin, View):
 
     def get(self, request):
 
-        racks = Rack.objects.select_related(
-            'site', 'group', 'tenant', 'role'
-        ).prefetch_related(
-            'devices__device_type'
-        )
+        racks = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role', 'devices__device_type')
         racks = filters.RackFilter(request.GET, racks).qs
         total_count = racks.count()
 
@@ -402,15 +396,18 @@ class RackView(PermissionRequiredMixin, View):
 
     def get(self, request, pk):
 
-        rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
+        rack = get_object_or_404(Rack.objects.prefetch_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
 
-        nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True) \
-            .select_related('device_type__manufacturer')
+        nonracked_devices = Device.objects.filter(
+            rack=rack,
+            position__isnull=True,
+            parent_bay__isnull=True
+        ).prefetch_related('device_type__manufacturer')
         next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
         prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
 
         reservations = RackReservation.objects.filter(rack=rack)
-        power_feeds = PowerFeed.objects.filter(rack=rack).select_related('power_panel')
+        power_feeds = PowerFeed.objects.filter(rack=rack).prefetch_related('power_panel')
 
         return render(request, 'dcim/rack.html', {
             'rack': rack,
@@ -451,7 +448,7 @@ class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_rack'
-    queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role')
+    queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role')
     filter = filters.RackFilter
     table = tables.RackTable
     form = forms.RackBulkEditForm
@@ -460,7 +457,7 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_rack'
-    queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role')
+    queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role')
     filter = filters.RackFilter
     table = tables.RackTable
     default_return_url = 'dcim:rack_list'
@@ -472,7 +469,7 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class RackReservationListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_rackreservation'
-    queryset = RackReservation.objects.select_related('rack__site')
+    queryset = RackReservation.objects.prefetch_related('rack__site')
     filter = filters.RackReservationFilter
     filter_form = forms.RackReservationFilterForm
     table = tables.RackReservationTable
@@ -508,7 +505,7 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 
 class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_rackreservation'
-    queryset = RackReservation.objects.select_related('rack', 'user')
+    queryset = RackReservation.objects.prefetch_related('rack', 'user')
     filter = filters.RackReservationFilter
     table = tables.RackReservationTable
     form = forms.RackReservationBulkEditForm
@@ -517,7 +514,7 @@ class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_rackreservation'
-    queryset = RackReservation.objects.select_related('rack', 'user')
+    queryset = RackReservation.objects.prefetch_related('rack', 'user')
     filter = filters.RackReservationFilter
     table = tables.RackReservationTable
     default_return_url = 'dcim:rackreservation_list'
@@ -569,7 +566,7 @@ class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class DeviceTypeListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_devicetype'
-    queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
+    queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances'))
     filter = filters.DeviceTypeFilter
     filter_form = forms.DeviceTypeFilterForm
     table = tables.DeviceTypeTable
@@ -666,7 +663,7 @@ class DeviceTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_devicetype'
-    queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
+    queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances'))
     filter = filters.DeviceTypeFilter
     table = tables.DeviceTypeTable
     form = forms.DeviceTypeBulkEditForm
@@ -675,7 +672,7 @@ class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_devicetype'
-    queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
+    queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances'))
     filter = filters.DeviceTypeFilter
     table = tables.DeviceTypeTable
     default_return_url = 'dcim:devicetype_list'
@@ -907,7 +904,7 @@ class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class DeviceListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_device'
-    queryset = Device.objects.select_related(
+    queryset = Device.objects.prefetch_related(
         'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6'
     )
     filter = filters.DeviceFilter
@@ -921,7 +918,7 @@ class DeviceView(PermissionRequiredMixin, View):
 
     def get(self, request, pk):
 
-        device = get_object_or_404(Device.objects.select_related(
+        device = get_object_or_404(Device.objects.prefetch_related(
             'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
         ), pk=pk)
 
@@ -934,32 +931,31 @@ class DeviceView(PermissionRequiredMixin, View):
             vc_members = []
 
         # Console ports
-        console_ports = device.consoleports.select_related('connected_endpoint__device', 'cable')
+        console_ports = device.consoleports.prefetch_related('connected_endpoint__device', 'cable')
 
         # Console server ports
-        consoleserverports = device.consoleserverports.select_related('connected_endpoint__device', 'cable')
+        consoleserverports = device.consoleserverports.prefetch_related('connected_endpoint__device', 'cable')
 
         # Power ports
-        power_ports = device.powerports.select_related('_connected_poweroutlet__device', 'cable')
+        power_ports = device.powerports.prefetch_related('_connected_poweroutlet__device', 'cable')
 
         # Power outlets
-        poweroutlets = device.poweroutlets.select_related('connected_endpoint__device', 'cable', 'power_port')
+        poweroutlets = device.poweroutlets.prefetch_related('connected_endpoint__device', 'cable', 'power_port')
 
         # Interfaces
-        interfaces = device.vc_interfaces.select_related(
-            'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable'
-        ).prefetch_related(
+        interfaces = device.vc_interfaces.prefetch_related(
+            'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable',
             'cable__termination_a', 'cable__termination_b', 'ip_addresses', 'tags'
         )
 
         # Front ports
-        front_ports = device.frontports.select_related('rear_port', 'cable')
+        front_ports = device.frontports.prefetch_related('rear_port', 'cable')
 
         # Rear ports
-        rear_ports = device.rearports.select_related('cable')
+        rear_ports = device.rearports.prefetch_related('cable')
 
         # Device bays
-        device_bays = device.device_bays.select_related('installed_device__device_type__manufacturer')
+        device_bays = device.device_bays.prefetch_related('installed_device__device_type__manufacturer')
 
         # Services
         services = device.services.all()
@@ -972,7 +968,7 @@ class DeviceView(PermissionRequiredMixin, View):
             site=device.site, device_role=device.device_role
         ).exclude(
             pk=device.pk
-        ).select_related(
+        ).prefetch_related(
             'rack', 'device_type__manufacturer'
         )[:10]
 
@@ -1005,10 +1001,8 @@ class DeviceInventoryView(PermissionRequiredMixin, View):
         device = get_object_or_404(Device, pk=pk)
         inventory_items = InventoryItem.objects.filter(
             device=device, parent=None
-        ).select_related(
-            'manufacturer'
         ).prefetch_related(
-            'child_items'
+            'manufacturer', 'child_items'
         )
 
         return render(request, 'dcim/device_inventory.html', {
@@ -1037,7 +1031,7 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
     def get(self, request, pk):
 
         device = get_object_or_404(Device, pk=pk)
-        interfaces = device.vc_interfaces.connectable().select_related(
+        interfaces = device.vc_interfaces.connectable().prefetch_related(
             '_connected_interface__device'
         )
 
@@ -1114,7 +1108,7 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_device'
-    queryset = Device.objects.select_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
+    queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
     filter = filters.DeviceFilter
     table = tables.DeviceTable
     form = forms.DeviceBulkEditForm
@@ -1123,7 +1117,7 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_device'
-    queryset = Device.objects.select_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
+    queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
     filter = filters.DeviceFilter
     table = tables.DeviceTable
     default_return_url = 'dcim:device_list'
@@ -1310,7 +1304,7 @@ class InterfaceView(PermissionRequiredMixin, View):
 
         # Get assigned IP addresses
         ipaddress_table = InterfaceIPAddressTable(
-            data=interface.ip_addresses.select_related('vrf', 'tenant'),
+            data=interface.ip_addresses.prefetch_related('vrf', 'tenant'),
             orderable=False
         )
 
@@ -1319,7 +1313,7 @@ class InterfaceView(PermissionRequiredMixin, View):
         if interface.untagged_vlan is not None:
             vlans.append(interface.untagged_vlan)
             vlans[0].tagged = False
-        for vlan in interface.tagged_vlans.select_related('site', 'group', 'tenant', 'role'):
+        for vlan in interface.tagged_vlans.prefetch_related('site', 'group', 'tenant', 'role'):
             vlan.tagged = True
             vlans.append(vlan)
         vlan_table = InterfaceVLANTable(
@@ -1842,7 +1836,7 @@ class CableBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class ConsoleConnectionsListView(PermissionRequiredMixin, ObjectListView):
     permission_required = ('dcim.view_consoleport', 'dcim.view_consoleserverport')
-    queryset = ConsolePort.objects.select_related(
+    queryset = ConsolePort.objects.prefetch_related(
         'device', 'connected_endpoint__device'
     ).filter(
         connected_endpoint__isnull=False
@@ -1873,7 +1867,7 @@ class ConsoleConnectionsListView(PermissionRequiredMixin, ObjectListView):
 
 class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView):
     permission_required = ('dcim.view_powerport', 'dcim.view_poweroutlet')
-    queryset = PowerPort.objects.select_related(
+    queryset = PowerPort.objects.prefetch_related(
         'device', '_connected_poweroutlet__device'
     ).filter(
         _connected_poweroutlet__isnull=False
@@ -1904,7 +1898,7 @@ class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView):
 
 class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_interface'
-    queryset = Interface.objects.select_related(
+    queryset = Interface.objects.prefetch_related(
         'device', 'cable', '_connected_interface__device'
     ).filter(
         # Avoid duplicate connections by only selecting the lower PK in a connected pair
@@ -1947,7 +1941,7 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
 
 class InventoryItemListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_inventoryitem'
-    queryset = InventoryItem.objects.select_related('device', 'manufacturer')
+    queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer')
     filter = filters.InventoryItemFilter
     filter_form = forms.InventoryItemFilterForm
     table = tables.InventoryItemTable
@@ -1982,7 +1976,7 @@ class InventoryItemBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_inventoryitem'
-    queryset = InventoryItem.objects.select_related('device', 'manufacturer')
+    queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer')
     filter = filters.InventoryItemFilter
     table = tables.InventoryItemTable
     form = forms.InventoryItemBulkEditForm
@@ -1991,7 +1985,7 @@ class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_inventoryitem'
-    queryset = InventoryItem.objects.select_related('device', 'manufacturer')
+    queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer')
     table = tables.InventoryItemTable
     template_name = 'dcim/inventoryitem_bulk_delete.html'
     default_return_url = 'dcim:inventoryitem_list'
@@ -2003,7 +1997,7 @@ class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class VirtualChassisListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_virtualchassis'
-    queryset = VirtualChassis.objects.select_related('master').annotate(member_count=Count('members'))
+    queryset = VirtualChassis.objects.prefetch_related('master').annotate(member_count=Count('members'))
     table = tables.VirtualChassisTable
     filter = filters.VirtualChassisFilter
     filter_form = forms.VirtualChassisFilterForm
@@ -2023,7 +2017,7 @@ class VirtualChassisCreateView(PermissionRequiredMixin, View):
             return redirect('dcim:device_list')
         device_queryset = Device.objects.filter(
             pk__in=pk_form.cleaned_data.get('pk')
-        ).select_related('rack').order_by('vc_position')
+        ).prefetch_related('rack').order_by('vc_position')
 
         VCMemberFormSet = modelformset_factory(
             model=Device,
@@ -2077,7 +2071,7 @@ class VirtualChassisEditView(PermissionRequiredMixin, GetReturnURLMixin, View):
             formset=forms.BaseVCMemberFormSet,
             extra=0
         )
-        members_queryset = virtual_chassis.members.select_related('rack').order_by('vc_position')
+        members_queryset = virtual_chassis.members.prefetch_related('rack').order_by('vc_position')
 
         vc_form = forms.VirtualChassisForm(instance=virtual_chassis)
         vc_form.fields['master'].queryset = members_queryset
@@ -2098,7 +2092,7 @@ class VirtualChassisEditView(PermissionRequiredMixin, GetReturnURLMixin, View):
             formset=forms.BaseVCMemberFormSet,
             extra=0
         )
-        members_queryset = virtual_chassis.members.select_related('rack').order_by('vc_position')
+        members_queryset = virtual_chassis.members.prefetch_related('rack').order_by('vc_position')
 
         vc_form = forms.VirtualChassisForm(request.POST, instance=virtual_chassis)
         vc_form.fields['master'].queryset = members_queryset
@@ -2114,7 +2108,7 @@ class VirtualChassisEditView(PermissionRequiredMixin, GetReturnURLMixin, View):
                 # Nullify the vc_position of each member first to allow reordering without raising an IntegrityError on
                 # duplicate positions. Then save each member instance.
                 members = formset.save(commit=False)
-                Device.objects.filter(pk__in=[m.pk for m in members]).update(vc_position=None)
+                Device.objects.filter(pk__in=[m.pk for m in members]).invalidated_update(vc_position=None)
                 for member in members:
                     member.save()
 
@@ -2215,12 +2209,13 @@ class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin,
 
         if form.is_valid():
 
-            Device.objects.filter(pk=device.pk).update(
+            Device.objects.filter(pk=device.pk).invalidated_update(
                 virtual_chassis=None,
                 vc_position=None,
                 vc_priority=None
             )
 
+
             msg = 'Removed {} from virtual chassis {}'.format(device, device.virtual_chassis)
             messages.success(request, msg)
 
@@ -2239,7 +2234,7 @@ class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin,
 
 class PowerPanelListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_powerpanel'
-    queryset = PowerPanel.objects.select_related(
+    queryset = PowerPanel.objects.prefetch_related(
         'site', 'rack_group'
     ).annotate(
         powerfeed_count=Count('powerfeeds')
@@ -2255,9 +2250,9 @@ class PowerPanelView(PermissionRequiredMixin, View):
 
     def get(self, request, pk):
 
-        powerpanel = get_object_or_404(PowerPanel.objects.select_related('site', 'rack_group'), pk=pk)
+        powerpanel = get_object_or_404(PowerPanel.objects.prefetch_related('site', 'rack_group'), pk=pk)
         powerfeed_table = tables.PowerFeedTable(
-            data=PowerFeed.objects.filter(power_panel=powerpanel).select_related('rack'),
+            data=PowerFeed.objects.filter(power_panel=powerpanel).prefetch_related('rack'),
             orderable=False
         )
         powerfeed_table.exclude = ['power_panel']
@@ -2294,7 +2289,7 @@ class PowerPanelBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class PowerPanelBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_powerpanel'
-    queryset = PowerPanel.objects.select_related(
+    queryset = PowerPanel.objects.prefetch_related(
         'site', 'rack_group'
     ).annotate(
         rack_count=Count('powerfeeds')
@@ -2310,7 +2305,7 @@ class PowerPanelBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class PowerFeedListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_powerfeed'
-    queryset = PowerFeed.objects.select_related(
+    queryset = PowerFeed.objects.prefetch_related(
         'power_panel', 'rack'
     )
     filter = filters.PowerFeedFilter
@@ -2324,7 +2319,7 @@ class PowerFeedView(PermissionRequiredMixin, View):
 
     def get(self, request, pk):
 
-        powerfeed = get_object_or_404(PowerFeed.objects.select_related('power_panel', 'rack'), pk=pk)
+        powerfeed = get_object_or_404(PowerFeed.objects.prefetch_related('power_panel', 'rack'), pk=pk)
 
         return render(request, 'dcim/powerfeed.html', {
             'powerfeed': powerfeed,
@@ -2358,7 +2353,7 @@ class PowerFeedBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class PowerFeedBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_powerfeed'
-    queryset = PowerFeed.objects.select_related('power_panel', 'rack')
+    queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
     filter = filters.PowerFeedFilter
     table = tables.PowerFeedTable
     form = forms.PowerFeedBulkEditForm
@@ -2367,7 +2362,7 @@ class PowerFeedBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class PowerFeedBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_powerfeed'
-    queryset = PowerFeed.objects.select_related('power_panel', 'rack')
+    queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
     filter = filters.PowerFeedFilter
     table = tables.PowerFeedTable
     default_return_url = 'dcim:powerfeed_list'

+ 2 - 2
netbox/extras/api/views.py

@@ -120,7 +120,7 @@ class ExportTemplateViewSet(ModelViewSet):
 #
 
 class TopologyMapViewSet(ModelViewSet):
-    queryset = TopologyMap.objects.select_related('site')
+    queryset = TopologyMap.objects.prefetch_related('site')
     serializer_class = serializers.TopologyMapSerializer
     filterset_class = filters.TopologyMapFilter
 
@@ -260,6 +260,6 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
     """
     Retrieve a list of recent changes.
     """
-    queryset = ObjectChange.objects.select_related('user')
+    queryset = ObjectChange.objects.prefetch_related('user')
     serializer_class = serializers.ObjectChangeSerializer
     filterset_class = filters.ObjectChangeFilter

+ 9 - 5
netbox/extras/forms.py

@@ -111,8 +111,10 @@ class CustomFieldForm(forms.ModelForm):
 
         # If editing an existing object, initialize values for all custom fields
         if self.instance.pk:
-            existing_values = CustomFieldValue.objects.filter(obj_type=self.obj_type, obj_id=self.instance.pk)\
-                .select_related('field')
+            existing_values = CustomFieldValue.objects.filter(
+                obj_type=self.obj_type,
+                obj_id=self.instance.pk
+            ).prefetch_related('field')
             for cfv in existing_values:
                 self.initial['cf_{}'.format(str(cfv.field.name))] = cfv.serialized_value
 
@@ -120,9 +122,11 @@ class CustomFieldForm(forms.ModelForm):
 
         for field_name in self.custom_fields:
             try:
-                cfv = CustomFieldValue.objects.select_related('field').get(field=self.fields[field_name].model,
-                                                                           obj_type=self.obj_type,
-                                                                           obj_id=self.instance.pk)
+                cfv = CustomFieldValue.objects.prefetch_related('field').get(
+                    field=self.fields[field_name].model,
+                    obj_type=self.obj_type,
+                    obj_id=self.instance.pk
+                )
             except CustomFieldValue.DoesNotExist:
                 # Skip this field if none exists already and its value is empty
                 if self.cleaned_data[field_name] in [None, '']:

+ 2 - 2
netbox/extras/models.py

@@ -569,7 +569,7 @@ class TopologyMap(models.Model):
             # Add each device to the graph
             devices = []
             for query in device_set.strip(';').split(';'):  # Split regexes on semicolons
-                devices += Device.objects.filter(name__regex=query).select_related('device_role')
+                devices += Device.objects.filter(name__regex=query).prefetch_related('device_role')
             # Remove duplicate devices
             devices = [d for d in devices if d.id not in seen]
             seen.update([d.id for d in devices])
@@ -607,7 +607,7 @@ class TopologyMap(models.Model):
         from dcim.models import Interface
 
         # Add all interface connections to the graph
-        connected_interfaces = Interface.objects.select_related(
+        connected_interfaces = Interface.objects.prefetch_related(
             '_connected_interface__device'
         ).filter(
             Q(device__in=devices) | Q(_connected_interface__device__in=devices),

+ 3 - 5
netbox/extras/views.py

@@ -47,10 +47,8 @@ class TagView(View):
         tag = get_object_or_404(Tag, slug=slug)
         tagged_items = TaggedItem.objects.filter(
             tag=tag
-        ).select_related(
-            'content_type'
         ).prefetch_related(
-            'content_object'
+            'content_type', 'content_object'
         )
 
         # Generate a table of all items tagged with this Tag
@@ -178,7 +176,7 @@ class ObjectConfigContextView(View):
 
 class ObjectChangeListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'extras.view_objectchange'
-    queryset = ObjectChange.objects.select_related('user', 'changed_object_type')
+    queryset = ObjectChange.objects.prefetch_related('user', 'changed_object_type')
     filter = filters.ObjectChangeFilter
     filter_form = ObjectChangeFilterForm
     table = ObjectChangeTable
@@ -217,7 +215,7 @@ class ObjectChangeLogView(View):
 
         # Gather all changes for this object (and its related objects)
         content_type = ContentType.objects.get_for_model(model)
-        objectchanges = ObjectChange.objects.select_related(
+        objectchanges = ObjectChange.objects.prefetch_related(
             'user', 'changed_object_type'
         ).filter(
             Q(changed_object_type=content_type, changed_object_id=obj.pk) |

+ 9 - 16
netbox/ipam/api/views.py

@@ -33,7 +33,7 @@ class IPAMFieldChoicesViewSet(FieldChoicesViewSet):
 #
 
 class VRFViewSet(CustomFieldModelViewSet):
-    queryset = VRF.objects.select_related('tenant').prefetch_related('tags').annotate(
+    queryset = VRF.objects.prefetch_related('tenant').prefetch_related('tags').annotate(
         ipaddress_count=get_subquery(IPAddress, 'vrf'),
         prefix_count=get_subquery(Prefix, 'vrf')
     )
@@ -58,7 +58,7 @@ class RIRViewSet(ModelViewSet):
 #
 
 class AggregateViewSet(CustomFieldModelViewSet):
-    queryset = Aggregate.objects.select_related('rir').prefetch_related('tags')
+    queryset = Aggregate.objects.prefetch_related('rir').prefetch_related('tags')
     serializer_class = serializers.AggregateSerializer
     filterset_class = filters.AggregateFilter
 
@@ -81,11 +81,7 @@ class RoleViewSet(ModelViewSet):
 #
 
 class PrefixViewSet(CustomFieldModelViewSet):
-    queryset = Prefix.objects.select_related(
-        'site', 'vrf__tenant', 'tenant', 'vlan', 'role'
-    ).prefetch_related(
-        'tags'
-    )
+    queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags')
     serializer_class = serializers.PrefixSerializer
     filterset_class = filters.PrefixFilter
 
@@ -263,9 +259,8 @@ class PrefixViewSet(CustomFieldModelViewSet):
 #
 
 class IPAddressViewSet(CustomFieldModelViewSet):
-    queryset = IPAddress.objects.select_related(
-        'vrf__tenant', 'tenant', 'nat_inside', 'interface__device__device_type', 'interface__virtual_machine'
-    ).prefetch_related(
+    queryset = IPAddress.objects.prefetch_related(
+        'vrf__tenant', 'tenant', 'nat_inside', 'interface__device__device_type', 'interface__virtual_machine',
         'nat_outside', 'tags',
     )
     serializer_class = serializers.IPAddressSerializer
@@ -277,7 +272,7 @@ class IPAddressViewSet(CustomFieldModelViewSet):
 #
 
 class VLANGroupViewSet(ModelViewSet):
-    queryset = VLANGroup.objects.select_related('site').annotate(
+    queryset = VLANGroup.objects.prefetch_related('site').annotate(
         vlan_count=Count('vlans')
     )
     serializer_class = serializers.VLANGroupSerializer
@@ -289,10 +284,8 @@ class VLANGroupViewSet(ModelViewSet):
 #
 
 class VLANViewSet(CustomFieldModelViewSet):
-    queryset = VLAN.objects.select_related(
-        'site', 'group', 'tenant', 'role'
-    ).prefetch_related(
-        'tags'
+    queryset = VLAN.objects.prefetch_related(
+        'site', 'group', 'tenant', 'role', 'tags'
     ).annotate(
         prefix_count=get_subquery(Prefix, 'role')
     )
@@ -305,6 +298,6 @@ class VLANViewSet(CustomFieldModelViewSet):
 #
 
 class ServiceViewSet(ModelViewSet):
-    queryset = Service.objects.select_related('device').prefetch_related('tags')
+    queryset = Service.objects.prefetch_related('device').prefetch_related('tags')
     serializer_class = serializers.ServiceSerializer
     filterset_class = filters.ServiceFilter

+ 1 - 1
netbox/ipam/filters.py

@@ -360,7 +360,7 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet):
 
     def filter_device(self, queryset, name, value):
         try:
-            device = Device.objects.select_related('device_type').get(**{name: value})
+            device = Device.objects.prefetch_related('device_type').get(**{name: value})
             vc_interface_ids = [i['id'] for i in device.vc_interfaces.values('id')]
             return queryset.filter(interface_id__in=vc_interface_ids)
         except Device.DoesNotExist:

+ 34 - 38
netbox/ipam/views.py

@@ -115,7 +115,7 @@ def add_available_vlans(vlan_group, vlans):
 
 class VRFListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'ipam.view_vrf'
-    queryset = VRF.objects.select_related('tenant')
+    queryset = VRF.objects.prefetch_related('tenant')
     filter = filters.VRFFilter
     filter_form = forms.VRFFilterForm
     table = tables.VRFTable
@@ -163,7 +163,7 @@ class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'ipam.change_vrf'
-    queryset = VRF.objects.select_related('tenant')
+    queryset = VRF.objects.prefetch_related('tenant')
     filter = filters.VRFFilter
     table = tables.VRFTable
     form = forms.VRFBulkEditForm
@@ -172,7 +172,7 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'ipam.delete_vrf'
-    queryset = VRF.objects.select_related('tenant')
+    queryset = VRF.objects.prefetch_related('tenant')
     filter = filters.VRFFilter
     table = tables.VRFTable
     default_return_url = 'ipam:vrf_list'
@@ -291,7 +291,7 @@ class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class AggregateListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'ipam.view_aggregate'
-    queryset = Aggregate.objects.select_related('rir').extra(select={
+    queryset = Aggregate.objects.prefetch_related('rir').extra(select={
         'child_count': 'SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix',
     })
     filter = filters.AggregateFilter
@@ -326,7 +326,7 @@ class AggregateView(PermissionRequiredMixin, View):
         # Find all child prefixes contained by this aggregate
         child_prefixes = Prefix.objects.filter(
             prefix__net_contained_or_equal=str(aggregate.prefix)
-        ).select_related(
+        ).prefetch_related(
             'site', 'role'
         ).annotate_depth(
             limit=0
@@ -384,7 +384,7 @@ class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'ipam.change_aggregate'
-    queryset = Aggregate.objects.select_related('rir')
+    queryset = Aggregate.objects.prefetch_related('rir')
     filter = filters.AggregateFilter
     table = tables.AggregateTable
     form = forms.AggregateBulkEditForm
@@ -393,7 +393,7 @@ class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'ipam.delete_aggregate'
-    queryset = Aggregate.objects.select_related('rir')
+    queryset = Aggregate.objects.prefetch_related('rir')
     filter = filters.AggregateFilter
     table = tables.AggregateTable
     default_return_url = 'ipam:aggregate_list'
@@ -441,7 +441,7 @@ class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class PrefixListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'ipam.view_prefix'
-    queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
+    queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
     filter = filters.PrefixFilter
     filter_form = forms.PrefixFilterForm
     table = tables.PrefixDetailTable
@@ -458,7 +458,7 @@ class PrefixView(PermissionRequiredMixin, View):
 
     def get(self, request, pk):
 
-        prefix = get_object_or_404(Prefix.objects.select_related(
+        prefix = get_object_or_404(Prefix.objects.prefetch_related(
             'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
         ), pk=pk)
 
@@ -472,7 +472,7 @@ class PrefixView(PermissionRequiredMixin, View):
             Q(vrf=prefix.vrf) | Q(vrf__isnull=True)
         ).filter(
             prefix__net_contains=str(prefix.prefix)
-        ).select_related(
+        ).prefetch_related(
             'site', 'role'
         ).annotate_depth()
         parent_prefix_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
@@ -483,7 +483,7 @@ class PrefixView(PermissionRequiredMixin, View):
             vrf=prefix.vrf, prefix=str(prefix.prefix)
         ).exclude(
             pk=prefix.pk
-        ).select_related(
+        ).prefetch_related(
             'site', 'role'
         )
         duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False)
@@ -505,7 +505,7 @@ class PrefixPrefixesView(PermissionRequiredMixin, View):
         prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
 
         # Child prefixes table
-        child_prefixes = prefix.get_child_prefixes().select_related(
+        child_prefixes = prefix.get_child_prefixes().prefetch_related(
             'site', 'vlan', 'role',
         ).annotate_depth(limit=0)
 
@@ -548,7 +548,7 @@ class PrefixIPAddressesView(PermissionRequiredMixin, View):
         prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
 
         # Find all IPAddresses belonging to this Prefix
-        ipaddresses = prefix.get_child_ips().select_related(
+        ipaddresses = prefix.get_child_ips().prefetch_related(
             'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
         )
         ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
@@ -608,7 +608,7 @@ class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'ipam.change_prefix'
-    queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
+    queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
     filter = filters.PrefixFilter
     table = tables.PrefixTable
     form = forms.PrefixBulkEditForm
@@ -617,7 +617,7 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'ipam.delete_prefix'
-    queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
+    queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
     filter = filters.PrefixFilter
     table = tables.PrefixTable
     default_return_url = 'ipam:prefix_list'
@@ -629,10 +629,8 @@ class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class IPAddressListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'ipam.view_ipaddress'
-    queryset = IPAddress.objects.select_related(
-        'vrf__tenant', 'tenant', 'nat_inside'
-    ).prefetch_related(
-        'interface__device', 'interface__virtual_machine'
+    queryset = IPAddress.objects.prefetch_related(
+        'vrf__tenant', 'tenant', 'nat_inside', 'interface__device', 'interface__virtual_machine'
     )
     filter = filters.IPAddressFilter
     filter_form = forms.IPAddressFilterForm
@@ -645,12 +643,12 @@ class IPAddressView(PermissionRequiredMixin, View):
 
     def get(self, request, pk):
 
-        ipaddress = get_object_or_404(IPAddress.objects.select_related('vrf__tenant', 'tenant'), pk=pk)
+        ipaddress = get_object_or_404(IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'), pk=pk)
 
         # Parent prefixes table
         parent_prefixes = Prefix.objects.filter(
             vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip)
-        ).select_related(
+        ).prefetch_related(
             'site', 'role'
         )
         parent_prefixes_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
@@ -661,10 +659,8 @@ class IPAddressView(PermissionRequiredMixin, View):
             vrf=ipaddress.vrf, address=str(ipaddress.address)
         ).exclude(
             pk=ipaddress.pk
-        ).select_related(
-            'nat_inside'
         ).prefetch_related(
-            'interface__device'
+            'nat_inside', 'interface__device'
         )
         # Exclude anycast IPs if this IP is anycast
         if ipaddress.role == IPADDRESS_ROLE_ANYCAST:
@@ -742,7 +738,7 @@ class IPAddressAssignView(PermissionRequiredMixin, View):
 
         if form.is_valid():
 
-            queryset = IPAddress.objects.select_related(
+            queryset = IPAddress.objects.prefetch_related(
                 'vrf', 'tenant', 'interface__device', 'interface__virtual_machine'
             ).filter(
                 vrf=form.cleaned_data['vrf'],
@@ -781,7 +777,7 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'ipam.change_ipaddress'
-    queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
+    queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
     filter = filters.IPAddressFilter
     table = tables.IPAddressTable
     form = forms.IPAddressBulkEditForm
@@ -790,7 +786,7 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'ipam.delete_ipaddress'
-    queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
+    queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
     filter = filters.IPAddressFilter
     table = tables.IPAddressTable
     default_return_url = 'ipam:ipaddress_list'
@@ -802,7 +798,7 @@ class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class VLANGroupListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'ipam.view_vlangroup'
-    queryset = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
+    queryset = VLANGroup.objects.prefetch_related('site').annotate(vlan_count=Count('vlans'))
     filter = filters.VLANGroupFilter
     filter_form = forms.VLANGroupFilterForm
     table = tables.VLANGroupTable
@@ -829,7 +825,7 @@ class VLANGroupBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'ipam.delete_vlangroup'
-    queryset = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
+    queryset = VLANGroup.objects.prefetch_related('site').annotate(vlan_count=Count('vlans'))
     filter = filters.VLANGroupFilter
     table = tables.VLANGroupTable
     default_return_url = 'ipam:vlangroup_list'
@@ -878,7 +874,7 @@ class VLANGroupVLANsView(PermissionRequiredMixin, View):
 
 class VLANListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'ipam.view_vlan'
-    queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes')
+    queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes')
     filter = filters.VLANFilter
     filter_form = forms.VLANFilterForm
     table = tables.VLANDetailTable
@@ -890,10 +886,10 @@ class VLANView(PermissionRequiredMixin, View):
 
     def get(self, request, pk):
 
-        vlan = get_object_or_404(VLAN.objects.select_related(
+        vlan = get_object_or_404(VLAN.objects.prefetch_related(
             'site__region', 'tenant__group', 'role'
         ), pk=pk)
-        prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
+        prefixes = Prefix.objects.filter(vlan=vlan).prefetch_related('vrf', 'site', 'role')
         prefix_table = tables.PrefixTable(list(prefixes), orderable=False)
         prefix_table.exclude = ('vlan',)
 
@@ -909,7 +905,7 @@ class VLANMembersView(PermissionRequiredMixin, View):
     def get(self, request, pk):
 
         vlan = get_object_or_404(VLAN.objects.all(), pk=pk)
-        members = vlan.get_members().select_related('device', 'virtual_machine')
+        members = vlan.get_members().prefetch_related('device', 'virtual_machine')
 
         members_table = tables.VLANMemberTable(members)
 
@@ -953,7 +949,7 @@ class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'ipam.change_vlan'
-    queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
+    queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
     filter = filters.VLANFilter
     table = tables.VLANTable
     form = forms.VLANBulkEditForm
@@ -962,7 +958,7 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'ipam.delete_vlan'
-    queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
+    queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
     filter = filters.VLANFilter
     table = tables.VLANTable
     default_return_url = 'ipam:vlan_list'
@@ -974,7 +970,7 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class ServiceListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'ipam.view_service'
-    queryset = Service.objects.select_related('device', 'virtual_machine')
+    queryset = Service.objects.prefetch_related('device', 'virtual_machine')
     filter = filters.ServiceFilter
     filter_form = forms.ServiceFilterForm
     table = tables.ServiceTable
@@ -1021,7 +1017,7 @@ class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 
 class ServiceBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'ipam.change_service'
-    queryset = Service.objects.select_related('device', 'virtual_machine')
+    queryset = Service.objects.prefetch_related('device', 'virtual_machine')
     filter = filters.ServiceFilter
     table = tables.ServiceTable
     form = forms.ServiceBulkEditForm
@@ -1030,7 +1026,7 @@ class ServiceBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class ServiceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'ipam.delete_service'
-    queryset = Service.objects.select_related('device', 'virtual_machine')
+    queryset = Service.objects.prefetch_related('device', 'virtual_machine')
     filter = filters.ServiceFilter
     table = tables.ServiceTable
     default_return_url = 'ipam:service_list'

+ 1 - 1
netbox/netbox/api.py

@@ -37,7 +37,7 @@ class TokenAuthentication(authentication.TokenAuthentication):
     def authenticate_credentials(self, key):
         model = self.get_model()
         try:
-            token = model.objects.select_related('user').get(key=key)
+            token = model.objects.prefetch_related('user').get(key=key)
         except model.DoesNotExist:
             raise exceptions.AuthenticationFailed("Invalid token")
 

+ 17 - 17
netbox/netbox/views.py

@@ -46,38 +46,38 @@ SEARCH_TYPES = OrderedDict((
         'url': 'circuits:provider_list',
     }),
     ('circuit', {
-        'queryset': Circuit.objects.select_related('type', 'provider', 'tenant').prefetch_related('terminations__site'),
+        'queryset': Circuit.objects.prefetch_related('type', 'provider', 'tenant').prefetch_related('terminations__site'),
         'filter': CircuitFilter,
         'table': CircuitTable,
         'url': 'circuits:circuit_list',
     }),
     # DCIM
     ('site', {
-        'queryset': Site.objects.select_related('region', 'tenant'),
+        'queryset': Site.objects.prefetch_related('region', 'tenant'),
         'filter': SiteFilter,
         'table': SiteTable,
         'url': 'dcim:site_list',
     }),
     ('rack', {
-        'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role'),
+        'queryset': Rack.objects.prefetch_related('site', 'group', 'tenant', 'role'),
         'filter': RackFilter,
         'table': RackTable,
         'url': 'dcim:rack_list',
     }),
     ('rackgroup', {
-        'queryset': RackGroup.objects.select_related('site').annotate(rack_count=Count('racks')),
+        'queryset': RackGroup.objects.prefetch_related('site').annotate(rack_count=Count('racks')),
         'filter': RackGroupFilter,
         'table': RackGroupTable,
         'url': 'dcim:rackgroup_list',
     }),
     ('devicetype', {
-        'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')),
+        'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances')),
         'filter': DeviceTypeFilter,
         'table': DeviceTypeTable,
         'url': 'dcim:devicetype_list',
     }),
     ('device', {
-        'queryset': Device.objects.select_related(
+        'queryset': Device.objects.prefetch_related(
             'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6',
         ),
         'filter': DeviceFilter,
@@ -85,7 +85,7 @@ SEARCH_TYPES = OrderedDict((
         'url': 'dcim:device_list',
     }),
     ('virtualchassis', {
-        'queryset': VirtualChassis.objects.select_related('master').annotate(member_count=Count('members')),
+        'queryset': VirtualChassis.objects.prefetch_related('master').annotate(member_count=Count('members')),
         'filter': VirtualChassisFilter,
         'table': VirtualChassisTable,
         'url': 'dcim:virtualchassis_list',
@@ -104,58 +104,58 @@ SEARCH_TYPES = OrderedDict((
     }),
     # IPAM
     ('vrf', {
-        'queryset': VRF.objects.select_related('tenant'),
+        'queryset': VRF.objects.prefetch_related('tenant'),
         'filter': VRFFilter,
         'table': VRFTable,
         'url': 'ipam:vrf_list',
     }),
     ('aggregate', {
-        'queryset': Aggregate.objects.select_related('rir'),
+        'queryset': Aggregate.objects.prefetch_related('rir'),
         'filter': AggregateFilter,
         'table': AggregateTable,
         'url': 'ipam:aggregate_list',
     }),
     ('prefix', {
-        'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
+        'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
         'filter': PrefixFilter,
         'table': PrefixTable,
         'url': 'ipam:prefix_list',
     }),
     ('ipaddress', {
-        'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant'),
+        'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'),
         'filter': IPAddressFilter,
         'table': IPAddressTable,
         'url': 'ipam:ipaddress_list',
     }),
     ('vlan', {
-        'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role'),
+        'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role'),
         'filter': VLANFilter,
         'table': VLANTable,
         'url': 'ipam:vlan_list',
     }),
     # Secrets
     ('secret', {
-        'queryset': Secret.objects.select_related('role', 'device'),
+        'queryset': Secret.objects.prefetch_related('role', 'device'),
         'filter': SecretFilter,
         'table': SecretTable,
         'url': 'secrets:secret_list',
     }),
     # Tenancy
     ('tenant', {
-        'queryset': Tenant.objects.select_related('group'),
+        'queryset': Tenant.objects.prefetch_related('group'),
         'filter': TenantFilter,
         'table': TenantTable,
         'url': 'tenancy:tenant_list',
     }),
     # Virtualization
     ('cluster', {
-        'queryset': Cluster.objects.select_related('type', 'group'),
+        'queryset': Cluster.objects.prefetch_related('type', 'group'),
         'filter': ClusterFilter,
         'table': ClusterTable,
         'url': 'virtualization:cluster_list',
     }),
     ('virtualmachine', {
-        'queryset': VirtualMachine.objects.select_related(
+        'queryset': VirtualMachine.objects.prefetch_related(
             'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
         ),
         'filter': VirtualMachineFilter,
@@ -224,7 +224,7 @@ class HomeView(View):
             'stats': stats,
             'topology_maps': TopologyMap.objects.filter(site__isnull=True),
             'report_results': ReportResult.objects.order_by('-created')[:10],
-            'changelog': ObjectChange.objects.select_related('user', 'changed_object_type')[:50]
+            'changelog': ObjectChange.objects.prefetch_related('user', 'changed_object_type')[:50]
         })
 
 

+ 2 - 4
netbox/secrets/api/views.py

@@ -46,10 +46,8 @@ class SecretRoleViewSet(ModelViewSet):
 #
 
 class SecretViewSet(ModelViewSet):
-    queryset = Secret.objects.select_related(
-        'device__primary_ip4', 'device__primary_ip6', 'role',
-    ).prefetch_related(
-        'role__users', 'role__groups', 'tags',
+    queryset = Secret.objects.prefetch_related(
+        'device__primary_ip4', 'device__primary_ip6', 'role', 'role__users', 'role__groups', 'tags',
     )
     serializer_class = serializers.SecretSerializer
     filterset_class = filters.SecretFilter

+ 3 - 3
netbox/secrets/views.py

@@ -69,7 +69,7 @@ class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class SecretListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'secrets.view_secret'
-    queryset = Secret.objects.select_related('role', 'device')
+    queryset = Secret.objects.prefetch_related('role', 'device')
     filter = filters.SecretFilter
     filter_form = forms.SecretFilterForm
     table = tables.SecretTable
@@ -247,7 +247,7 @@ class SecretBulkImportView(BulkImportView):
 
 class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'secrets.change_secret'
-    queryset = Secret.objects.select_related('role', 'device')
+    queryset = Secret.objects.prefetch_related('role', 'device')
     filter = filters.SecretFilter
     table = tables.SecretTable
     form = forms.SecretBulkEditForm
@@ -256,7 +256,7 @@ class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'secrets.delete_secret'
-    queryset = Secret.objects.select_related('role', 'device')
+    queryset = Secret.objects.prefetch_related('role', 'device')
     filter = filters.SecretFilter
     table = tables.SecretTable
     default_return_url = 'secrets:secret_list'

+ 2 - 4
netbox/tenancy/api/views.py

@@ -35,10 +35,8 @@ class TenantGroupViewSet(ModelViewSet):
 #
 
 class TenantViewSet(CustomFieldModelViewSet):
-    queryset = Tenant.objects.select_related(
-        'group'
-    ).prefetch_related(
-        'tags'
+    queryset = Tenant.objects.prefetch_related(
+        'group', 'tags'
     ).annotate(
         circuit_count=get_subquery(Circuit, 'tenant'),
         device_count=get_subquery(Device, 'tenant'),

+ 3 - 3
netbox/tenancy/views.py

@@ -56,7 +56,7 @@ class TenantGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class TenantListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'tenancy.view_tenant'
-    queryset = Tenant.objects.select_related('group')
+    queryset = Tenant.objects.prefetch_related('group')
     filter = filters.TenantFilter
     filter_form = forms.TenantFilterForm
     table = tables.TenantTable
@@ -115,7 +115,7 @@ class TenantBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class TenantBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'tenancy.change_tenant'
-    queryset = Tenant.objects.select_related('group')
+    queryset = Tenant.objects.prefetch_related('group')
     filter = filters.TenantFilter
     table = tables.TenantTable
     form = forms.TenantBulkEditForm
@@ -124,7 +124,7 @@ class TenantBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class TenantBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'tenancy.delete_tenant'
-    queryset = Tenant.objects.select_related('group')
+    queryset = Tenant.objects.prefetch_related('group')
     filter = filters.TenantFilter
     table = tables.TenantTable
     default_return_url = 'tenancy:tenant_list'

+ 8 - 8
netbox/virtualization/api/views.py

@@ -40,10 +40,8 @@ class ClusterGroupViewSet(ModelViewSet):
 
 
 class ClusterViewSet(CustomFieldModelViewSet):
-    queryset = Cluster.objects.select_related(
-        'type', 'group', 'site',
-    ).prefetch_related(
-        'tags'
+    queryset = Cluster.objects.prefetch_related(
+        'type', 'group', 'site', 'tags'
     ).annotate(
         device_count=get_subquery(Device, 'cluster'),
         virtualmachine_count=get_subquery(VirtualMachine, 'cluster')
@@ -57,9 +55,9 @@ class ClusterViewSet(CustomFieldModelViewSet):
 #
 
 class VirtualMachineViewSet(CustomFieldModelViewSet):
-    queryset = VirtualMachine.objects.select_related(
-        'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6'
-    ).prefetch_related('tags')
+    queryset = VirtualMachine.objects.prefetch_related(
+        'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
+    )
     filterset_class = filters.VirtualMachineFilter
 
     def get_serializer_class(self):
@@ -86,7 +84,9 @@ class VirtualMachineViewSet(CustomFieldModelViewSet):
 class InterfaceViewSet(ModelViewSet):
     queryset = Interface.objects.filter(
         virtual_machine__isnull=False
-    ).select_related('virtual_machine').prefetch_related('tags')
+    ).prefetch_related(
+        'virtual_machine', 'tags'
+    )
     serializer_class = serializers.InterfaceSerializer
     filterset_class = filters.InterfaceFilter
 

+ 2 - 2
netbox/virtualization/forms.py

@@ -376,7 +376,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
             for family in [4, 6]:
                 ip_choices = [(None, '---------')]
                 # Collect interface IPs
-                interface_ips = IPAddress.objects.select_related('interface').filter(
+                interface_ips = IPAddress.objects.prefetch_related('interface').filter(
                     family=family, interface__virtual_machine=self.instance
                 )
                 if interface_ips:
@@ -386,7 +386,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
                         ])
                     )
                 # Collect NAT IPs
-                nat_ips = IPAddress.objects.select_related('nat_inside').filter(
+                nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
                     family=family, nat_inside__interface__virtual_machine=self.instance
                 )
                 if nat_ips:

+ 8 - 8
netbox/virtualization/views.py

@@ -96,7 +96,7 @@ class ClusterGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class ClusterListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'virtualization.view_cluster'
-    queryset = Cluster.objects.select_related('type', 'group', 'site')
+    queryset = Cluster.objects.prefetch_related('type', 'group', 'site')
     table = tables.ClusterTable
     filter = filters.ClusterFilter
     filter_form = forms.ClusterFilterForm
@@ -109,7 +109,7 @@ class ClusterView(PermissionRequiredMixin, View):
     def get(self, request, pk):
 
         cluster = get_object_or_404(Cluster, pk=pk)
-        devices = Device.objects.filter(cluster=cluster).select_related(
+        devices = Device.objects.filter(cluster=cluster).prefetch_related(
             'site', 'rack', 'tenant', 'device_type__manufacturer'
         )
         device_table = DeviceTable(list(devices), orderable=False)
@@ -148,7 +148,7 @@ class ClusterBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class ClusterBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'virtualization.change_cluster'
-    queryset = Cluster.objects.select_related('type', 'group', 'site')
+    queryset = Cluster.objects.prefetch_related('type', 'group', 'site')
     filter = filters.ClusterFilter
     table = tables.ClusterTable
     form = forms.ClusterBulkEditForm
@@ -157,7 +157,7 @@ class ClusterBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class ClusterBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'virtualization.delete_cluster'
-    queryset = Cluster.objects.select_related('type', 'group', 'site')
+    queryset = Cluster.objects.prefetch_related('type', 'group', 'site')
     filter = filters.ClusterFilter
     table = tables.ClusterTable
     default_return_url = 'virtualization:cluster_list'
@@ -253,7 +253,7 @@ class ClusterRemoveDevicesView(PermissionRequiredMixin, View):
 
 class VirtualMachineListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'virtualization.view_virtualmachine'
-    queryset = VirtualMachine.objects.select_related('cluster', 'tenant', 'role', 'primary_ip4', 'primary_ip6')
+    queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role', 'primary_ip4', 'primary_ip6')
     filter = filters.VirtualMachineFilter
     filter_form = forms.VirtualMachineFilterForm
     table = tables.VirtualMachineDetailTable
@@ -265,7 +265,7 @@ class VirtualMachineView(PermissionRequiredMixin, View):
 
     def get(self, request, pk):
 
-        virtualmachine = get_object_or_404(VirtualMachine.objects.select_related('tenant__group'), pk=pk)
+        virtualmachine = get_object_or_404(VirtualMachine.objects.prefetch_related('tenant__group'), pk=pk)
         interfaces = Interface.objects.filter(virtual_machine=virtualmachine)
         services = Service.objects.filter(virtual_machine=virtualmachine)
 
@@ -309,7 +309,7 @@ class VirtualMachineBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 class VirtualMachineBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'virtualization.change_virtualmachine'
-    queryset = VirtualMachine.objects.select_related('cluster', 'tenant', 'role')
+    queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role')
     filter = filters.VirtualMachineFilter
     table = tables.VirtualMachineTable
     form = forms.VirtualMachineBulkEditForm
@@ -318,7 +318,7 @@ class VirtualMachineBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 class VirtualMachineBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'virtualization.delete_virtualmachine'
-    queryset = VirtualMachine.objects.select_related('cluster', 'tenant', 'role')
+    queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role')
     filter = filters.VirtualMachineFilter
     table = tables.VirtualMachineTable
     default_return_url = 'virtualization:virtualmachine_list'