@@ -543,6 +543,68 @@ def get_model_with_serializer(self):
543543 self .register_custom_object_search_index (model )
544544 return model
545545
546+ def _ensure_fk_constraints (self , model ):
547+ """
548+ Ensure that foreign key constraints are properly created at the database level
549+ for OBJECT type fields with ON DELETE CASCADE. This is necessary because models
550+ are created with managed=False, which may not properly create FK constraints
551+ with CASCADE behavior.
552+
553+ :param model: The model to ensure FK constraints for
554+ """
555+ # Query all OBJECT type fields for this CustomObjectType
556+ object_fields = self .fields .filter (type = CustomFieldTypeChoices .TYPE_OBJECT )
557+
558+ if not object_fields .exists ():
559+ return
560+
561+ table_name = self .get_database_table_name ()
562+
563+ with connection .cursor () as cursor :
564+ for field in object_fields :
565+ field_name = field .name
566+ try :
567+ model_field = model ._meta .get_field (field_name )
568+ if not (hasattr (model_field , 'remote_field' ) and model_field .remote_field ):
569+ continue
570+
571+ # Get the referenced table
572+ related_model = model_field .remote_field .model
573+ related_table = related_model ._meta .db_table
574+ column_name = model_field .column
575+
576+ # Drop existing FK constraint if it exists
577+ # Query for existing constraints
578+ cursor .execute ("""
579+ SELECT constraint_name
580+ FROM information_schema.table_constraints
581+ WHERE table_name = %s
582+ AND constraint_type = 'FOREIGN KEY'
583+ AND constraint_name LIKE %s
584+ """ , [table_name , f"%{ column_name } %" ])
585+
586+ for row in cursor .fetchall ():
587+ constraint_name = row [0 ]
588+ cursor .execute (f'ALTER TABLE "{ table_name } " DROP CONSTRAINT IF EXISTS "{ constraint_name } "' )
589+
590+ # Create new FK constraint with ON DELETE CASCADE
591+ constraint_name = f"{ table_name } _{ column_name } _fk_cascade"
592+ cursor .execute (f"""
593+ ALTER TABLE "{ table_name } "
594+ ADD CONSTRAINT "{ constraint_name } "
595+ FOREIGN KEY ("{ column_name } ")
596+ REFERENCES "{ related_table } " ("id")
597+ ON DELETE CASCADE
598+ DEFERRABLE INITIALLY DEFERRED
599+ """ )
600+
601+ except Exception as e :
602+ # Log the error but continue with other fields
603+ import logging
604+ logger = logging .getLogger ('netbox.custom_objects' )
605+ logger .warning (f"Failed to ensure FK constraint for { table_name } .{ field_name } : { e } " )
606+ continue
607+
546608 def create_model (self ):
547609 from netbox_custom_objects .api .serializers import get_serializer_class
548610 # Get the model and ensure it's registered
@@ -559,6 +621,9 @@ def create_model(self):
559621 with connection .schema_editor () as schema_editor :
560622 schema_editor .create_model (model )
561623
624+ # Ensure FK constraints are properly created for OBJECT fields
625+ self ._ensure_fk_constraints (model )
626+
562627 get_serializer_class (model )
563628 self .register_custom_object_search_index (model )
564629
@@ -1482,6 +1547,10 @@ def save(self, *args, **kwargs):
14821547 # Normal field alteration
14831548 schema_editor .alter_field (model , old_field , model_field )
14841549
1550+ # Ensure FK constraints are properly created for OBJECT fields with CASCADE behavior
1551+ if self .type == CustomFieldTypeChoices .TYPE_OBJECT :
1552+ self .custom_object_type ._ensure_fk_constraints (model )
1553+
14851554 # Clear and refresh the model cache for this CustomObjectType when a field is modified
14861555 self .custom_object_type .clear_model_cache (self .custom_object_type .id )
14871556
0 commit comments