#21355: Refactor trigger migrations (#22608)

This commit is contained in:
Jeremy Stretch 2026-07-07 12:04:45 -04:00 committed by GitHub
parent df83277156
commit 58e8068958
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 99 additions and 52 deletions

View File

@ -4,7 +4,7 @@ of the Python `post_save` handler formerly registered in netbox.denormalized.
"""
from django.db import migrations
from utilities.migration import cached_scope_triggers
from utilities.migration import InstallDenormalizationTrigger
class Migration(migrations.Migration):
@ -15,4 +15,24 @@ class Migration(migrations.Migration):
('dcim', '0238_ltree_paths'),
]
operations = cached_scope_triggers('circuits_circuittermination')
operations = [
InstallDenormalizationTrigger(
dependent_table='circuits_circuittermination',
source_table='dcim_site',
fk_column='_site_id',
mappings={'_region_id': 'region_id', '_site_group_id': 'group_id'},
),
InstallDenormalizationTrigger(
dependent_table='circuits_circuittermination',
source_table='dcim_location',
fk_column='_location_id',
mappings={'_site_id': 'site_id'},
related_mappings=(
{
'table': 'dcim_site',
'source_fk': 'site_id',
'mappings': {'_region_id': 'region_id', '_site_group_id': 'group_id'},
},
),
),
]

View File

@ -34,9 +34,7 @@ from .utils import create_cablepaths, rebuild_paths
def handle_location_site_change(instance, created, **kwargs):
"""
Cascade a Location's Site assignment down to the Racks, Devices, and PowerPanels it contains
(and to descendant Locations). The denormalized cache columns on cable terminations and device
components are maintained by PostgreSQL triggers, which fire on these Site/Location/Rack/Device
column writes.
(and to descendant Locations).
"""
if not created:
instance.get_descendants().update(site=instance.site)
@ -49,8 +47,7 @@ def handle_location_site_change(instance, created, **kwargs):
@receiver(post_save, sender=Rack)
def handle_rack_site_change(instance, created, **kwargs):
"""
Cascade a Rack's Site/Location assignment down to the Devices it contains. The denormalized cache
columns on those devices' components are maintained by PostgreSQL triggers.
Cascade a Rack's Site/Location assignment down to the Devices it contains.
"""
if not created:
Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location)

View File

@ -4,7 +4,7 @@ Python `post_save` handler formerly registered in netbox.denormalized (and dcim.
"""
from django.db import migrations
from utilities.migration import cached_scope_triggers
from utilities.migration import InstallDenormalizationTrigger
class Migration(migrations.Migration):
@ -15,4 +15,24 @@ class Migration(migrations.Migration):
('dcim', '0238_ltree_paths'),
]
operations = cached_scope_triggers('ipam_prefix')
operations = [
InstallDenormalizationTrigger(
dependent_table='ipam_prefix',
source_table='dcim_site',
fk_column='_site_id',
mappings={'_region_id': 'region_id', '_site_group_id': 'group_id'},
),
InstallDenormalizationTrigger(
dependent_table='ipam_prefix',
source_table='dcim_location',
fk_column='_location_id',
mappings={'_site_id': 'site_id'},
related_mappings=(
{
'table': 'dcim_site',
'source_fk': 'site_id',
'mappings': {'_region_id': 'region_id', '_site_group_id': 'group_id'},
},
),
),
]

View File

@ -4,7 +4,6 @@ from netbox.config import ConfigItem
__all__ = (
'InstallDenormalizationTrigger',
'cached_scope_triggers',
'custom_deconstruct',
)
@ -68,6 +67,15 @@ class InstallDenormalizationTrigger(migrations.operations.base.Operation):
newly created source row has no dependents yet) and it does not recurse: the dependent tables carry no
triggers of their own.
Example: refresh a CircuitTermination's cached region/sitegroup when its Site's region or group changes::
InstallDenormalizationTrigger(
dependent_table='circuits_circuittermination',
source_table='dcim_site',
fk_column='_site_id',
mappings={'_region_id': 'region_id', '_site_group_id': 'group_id'},
)
Note: this is a row-level trigger, so a bulk source update of N rows fires it N times. A statement-level
trigger with transition tables would batch this, but PostgreSQL forbids transition tables on a trigger
with an `UPDATE OF <columns>` list, and dropping that column list would fire the trigger on every source
@ -139,41 +147,3 @@ class InstallDenormalizationTrigger(migrations.operations.base.Operation):
def describe(self):
return f'Install denormalization trigger on {self.source_table} updating {self.dependent_table}'
# Site/region/site-group lookup shared by every CachedScopeMixin-style dependent (see cached_scope_triggers).
SITE_SCOPE_RELATED_MAPPINGS = (
{
'table': 'dcim_site',
'source_fk': 'site_id',
'mappings': {'_region_id': 'region_id', '_site_group_id': 'group_id'},
},
)
def cached_scope_triggers(dependent_table):
"""
Return the Site + Location `InstallDenormalizationTrigger` pair for a dependent table carrying the
standard cached-scope columns (_site/_location/_region/_site_group) i.e. any CachedScopeMixin model
(Prefix, Cluster, WirelessLAN) plus CircuitTermination, which share the same denormalization shape.
Region- and SiteGroup-scoped rows need no trigger: their cached FK is the scoped object itself and
never changes underneath them. So two triggers fully cover the cache:
- dcim_site: region/group changed -> refresh _region/_site_group on rows scoped to that site
- dcim_location: site changed -> refresh _site (and the new site's region/group)
"""
return [
InstallDenormalizationTrigger(
dependent_table=dependent_table,
source_table='dcim_site',
fk_column='_site_id',
mappings={'_region_id': 'region_id', '_site_group_id': 'group_id'},
),
InstallDenormalizationTrigger(
dependent_table=dependent_table,
source_table='dcim_location',
fk_column='_location_id',
mappings={'_site_id': 'site_id'},
related_mappings=SITE_SCOPE_RELATED_MAPPINGS,
),
]

View File

@ -4,7 +4,7 @@ via PostgreSQL triggers instead of the Python `dcim.signals.sync_cached_scope_fi
"""
from django.db import migrations
from utilities.migration import cached_scope_triggers
from utilities.migration import InstallDenormalizationTrigger
class Migration(migrations.Migration):
@ -15,4 +15,24 @@ class Migration(migrations.Migration):
('dcim', '0238_ltree_paths'),
]
operations = cached_scope_triggers('virtualization_cluster')
operations = [
InstallDenormalizationTrigger(
dependent_table='virtualization_cluster',
source_table='dcim_site',
fk_column='_site_id',
mappings={'_region_id': 'region_id', '_site_group_id': 'group_id'},
),
InstallDenormalizationTrigger(
dependent_table='virtualization_cluster',
source_table='dcim_location',
fk_column='_location_id',
mappings={'_site_id': 'site_id'},
related_mappings=(
{
'table': 'dcim_site',
'source_fk': 'site_id',
'mappings': {'_region_id': 'region_id', '_site_group_id': 'group_id'},
},
),
),
]

View File

@ -5,7 +5,7 @@ handler.
"""
from django.db import migrations
from utilities.migration import cached_scope_triggers
from utilities.migration import InstallDenormalizationTrigger
class Migration(migrations.Migration):
@ -16,4 +16,24 @@ class Migration(migrations.Migration):
('dcim', '0238_ltree_paths'),
]
operations = cached_scope_triggers('wireless_wirelesslan')
operations = [
InstallDenormalizationTrigger(
dependent_table='wireless_wirelesslan',
source_table='dcim_site',
fk_column='_site_id',
mappings={'_region_id': 'region_id', '_site_group_id': 'group_id'},
),
InstallDenormalizationTrigger(
dependent_table='wireless_wirelesslan',
source_table='dcim_location',
fk_column='_location_id',
mappings={'_site_id': 'site_id'},
related_mappings=(
{
'table': 'dcim_site',
'source_fk': 'site_id',
'mappings': {'_region_id': 'region_id', '_site_group_id': 'group_id'},
},
),
),
]