@@ -545,59 +545,73 @@ def get_model_with_serializer(self):
545545 self .register_custom_object_search_index (model )
546546 return model
547547
548- def _ensure_fk_constraints (self , model ):
548+ def _ensure_field_fk_constraint (self , model , field_name ):
549549 """
550- Ensure that foreign key constraints are properly created at the database level
551- for OBJECT type fields with ON DELETE CASCADE. This is necessary because models
552- are created with managed=False, which may not properly create FK constraints
550+ Ensure that a foreign key constraint is properly created at the database level
551+ for a specific OBJECT type field with ON DELETE CASCADE. This is necessary because
552+ models are created with managed=False, which may not properly create FK constraints
553553 with CASCADE behavior.
554554
555- :param model: The model to ensure FK constraints for
555+ :param model: The model containing the field
556+ :param field_name: The name of the field to ensure FK constraint for
556557 """
557- # Query all OBJECT type fields for this CustomObjectType
558- object_fields = self .fields .filter (type = CustomFieldTypeChoices .TYPE_OBJECT )
558+ table_name = self .get_database_table_name ()
559+
560+ # Get the model field
561+ try :
562+ model_field = model ._meta .get_field (field_name )
563+ except Exception :
564+ return
559565
560- if not object_fields . exists ( ):
566+ if not ( hasattr ( model_field , 'remote_field' ) and model_field . remote_field ):
561567 return
562568
563- table_name = self .get_database_table_name ()
569+ # Get the referenced table
570+ related_model = model_field .remote_field .model
571+ related_table = related_model ._meta .db_table
572+ column_name = model_field .column
564573
565574 with connection .cursor () as cursor :
566- for field in object_fields :
567- field_name = field .name
568- model_field = model ._meta .get_field (field_name )
569- if not (hasattr (model_field , 'remote_field' ) and model_field .remote_field ):
570- continue
575+ # Drop existing FK constraint if it exists
576+ # Query for existing constraints
577+ cursor .execute ("""
578+ SELECT constraint_name
579+ FROM information_schema.table_constraints
580+ WHERE table_name = %s
581+ AND constraint_type = 'FOREIGN KEY'
582+ AND constraint_name LIKE %s
583+ """ , [table_name , f"%{ column_name } %" ])
584+
585+ for row in cursor .fetchall ():
586+ constraint_name = row [0 ]
587+ cursor .execute (f'ALTER TABLE "{ table_name } " DROP CONSTRAINT IF EXISTS "{ constraint_name } "' )
588+
589+ # Create new FK constraint with ON DELETE CASCADE
590+ constraint_name = f"{ table_name } _{ column_name } _fk_cascade"
591+ cursor .execute (f"""
592+ ALTER TABLE "{ table_name } "
593+ ADD CONSTRAINT "{ constraint_name } "
594+ FOREIGN KEY ("{ column_name } ")
595+ REFERENCES "{ related_table } " ("id")
596+ ON DELETE CASCADE
597+ DEFERRABLE INITIALLY DEFERRED
598+ """ )
599+
600+ def _ensure_all_fk_constraints (self , model ):
601+ """
602+ Ensure that foreign key constraints are properly created at the database level
603+ for ALL OBJECT type fields with ON DELETE CASCADE.
571604
572- # Get the referenced table
573- related_model = model_field .remote_field .model
574- related_table = related_model ._meta .db_table
575- column_name = model_field .column
576-
577- # Drop existing FK constraint if it exists
578- # Query for existing constraints
579- cursor .execute ("""
580- SELECT constraint_name
581- FROM information_schema.table_constraints
582- WHERE table_name = %s
583- AND constraint_type = 'FOREIGN KEY'
584- AND constraint_name LIKE %s
585- """ , [table_name , f"%{ column_name } %" ])
586-
587- for row in cursor .fetchall ():
588- constraint_name = row [0 ]
589- cursor .execute (f'ALTER TABLE "{ table_name } " DROP CONSTRAINT IF EXISTS "{ constraint_name } "' )
590-
591- # Create new FK constraint with ON DELETE CASCADE
592- constraint_name = f"{ table_name } _{ column_name } _fk_cascade"
593- cursor .execute (f"""
594- ALTER TABLE "{ table_name } "
595- ADD CONSTRAINT "{ constraint_name } "
596- FOREIGN KEY ("{ column_name } ")
597- REFERENCES "{ related_table } " ("id")
598- ON DELETE CASCADE
599- DEFERRABLE INITIALLY DEFERRED
600- """ )
605+ NOTE: This method is deprecated for normal use - use _ensure_field_fk_constraint
606+ for individual fields. This is kept for migration purposes only.
607+
608+ :param model: The model to ensure FK constraints for
609+ """
610+ # Query all OBJECT type fields for this CustomObjectType
611+ object_fields = self .fields .filter (type = CustomFieldTypeChoices .TYPE_OBJECT )
612+
613+ for field in object_fields :
614+ self ._ensure_field_fk_constraint (model , field .name )
601615
602616 def create_model (self ):
603617 from netbox_custom_objects .api .serializers import get_serializer_class
@@ -615,8 +629,8 @@ def create_model(self):
615629 with connection .schema_editor () as schema_editor :
616630 schema_editor .create_model (model )
617631
618- # Ensure FK constraints are properly created for OBJECT fields
619- self . _ensure_fk_constraints ( model )
632+ # Note: FK constraints for OBJECT fields are now created when the field is saved,
633+ # not when the model is first created (since there are no fields yet )
620634
621635 get_serializer_class (model )
622636 self .register_custom_object_search_index (model )
@@ -855,6 +869,8 @@ def __init__(self, *args, **kwargs):
855869 super ().__init__ (* args , ** kwargs )
856870 self ._name = self .__dict__ .get ("name" )
857871 self ._original_name = self .name
872+ self ._original_type = self .type
873+ self ._original_related_object_type_id = self .related_object_type_id
858874
859875 def __str__ (self ):
860876 return self .label or self .name .replace ("_" , " " ).capitalize ()
@@ -1542,14 +1558,40 @@ def save(self, *args, **kwargs):
15421558 schema_editor .alter_field (model , old_field , model_field )
15431559
15441560 # Ensure FK constraints are properly created for OBJECT fields with CASCADE behavior
1561+ # Only do this when:
1562+ # 1. Creating a new OBJECT field, OR
1563+ # 2. Updating an existing field where type changed to OBJECT, OR
1564+ # 3. Updating an existing OBJECT field where related_object_type changed
1565+ should_ensure_fk = False
15451566 if self .type == CustomFieldTypeChoices .TYPE_OBJECT :
1546- self .custom_object_type ._ensure_fk_constraints (model )
1567+ if self ._state .adding :
1568+ # New OBJECT field - ensure FK constraint
1569+ should_ensure_fk = True
1570+ else :
1571+ # Existing field - check if type changed to OBJECT or related_object_type changed
1572+ type_changed_to_object = (
1573+ self ._original_type != CustomFieldTypeChoices .TYPE_OBJECT
1574+ and self .type == CustomFieldTypeChoices .TYPE_OBJECT
1575+ )
1576+ related_object_changed = (
1577+ self ._original_type == CustomFieldTypeChoices .TYPE_OBJECT
1578+ and self .related_object_type_id != self ._original_related_object_type_id
1579+ )
1580+ should_ensure_fk = type_changed_to_object or related_object_changed
15471581
15481582 # Clear and refresh the model cache for this CustomObjectType when a field is modified
15491583 self .custom_object_type .clear_model_cache (self .custom_object_type .id )
15501584
15511585 super ().save (* args , ** kwargs )
15521586
1587+ # Ensure FK constraints AFTER the transaction commits to avoid "pending trigger events" errors
1588+ # We use transaction.on_commit() to ensure all deferred constraints are checked first
1589+ if should_ensure_fk :
1590+ def ensure_constraint ():
1591+ self .custom_object_type ._ensure_field_fk_constraint (model , self .name )
1592+
1593+ transaction .on_commit (ensure_constraint )
1594+
15531595 # Reregister SearchIndex with new set of searchable fields
15541596 self .custom_object_type .register_custom_object_search_index (model )
15551597
0 commit comments