Safely empty or fill tables without violating any foreign key relationship

Le relazioni tra le tabelle di un database sono il meccanismo necessario a garantire l’integrità del dato.

Una tabella “figlia” non potrà, se opportunamente relazionata, contenere righe che possano legarsi a righe NON esistenti sulla tabella “padre” (Ad esempio: è impossibile che esistano delle righe di una fattura relative ad una riga di intestazione che non esiste).

 

Il fatto di avere dei legami tra questi oggetti evita anche, di conseguenza, che io possa cancellare delle righe “padre” che abbiano, in tabelle “figlie”, tuple che siano ad esse referenziate.

Per questo motivo è utile poter avere un modo per recuperare una sorta di elenco di tutte le tabelle, ordinato secondo il naturale ordine di relazione: prima le tabelle padre e quella senza relazioni, dopo le tabelle figlie (o figlie di figlie):

image

Nota bene: nell’esempio sopra la tabella [product] NON è volontariamente legata alla tabella [invoiceRow] solo per scopo di demo.

 

Avere questo ordinamento delle tabelle può esserci utile in due scenari:

  • popolare un database nell’ordine corretto (safely fill), ad esempio per avere una mole di dati simile a quella di produzione per effettuare verifiche di performance
  • svuotare un database nell’ordine corretto (safely empty), ad esempio per riportare le strutture al loro stato originario dopo una fase di test

 

Di seguito, grazie al meccanismo delle CTE ricorsive, un esempio di implementazione per ottenere il nostro ordinamento:

 WITH cteTables
    ( 
       tableName , 
        TableID , 
        ordinal 
    )
AS 
( 
    /* find all tables - anchor member for CTE */
    SELECT DISTINCT
       QUOTENAME(OBJECT_SCHEMA_NAME( o.id )) + '.' + QUOTENAME(OBJECT_NAME( o.id )) AS tableName, 
       o.id AS TableID , 
       0 AS ordinal
    FROM dbo.sysobjects o 
    INNER JOIN sys.all_columns c ON o.ID = c.object_id
    WHERE o.type = 'U'
        
    UNION ALL
        
    /* recursive member */
    SELECT 
       QUOTENAME(OBJECT_SCHEMA_NAME( o.id )) + '.' + QUOTENAME(OBJECT_NAME( o.id )) AS tableName, 
       o.id AS TableID , 
        cte.ordinal + 1 AS ordinal
    FROM dbo.sysobjects o 
    INNER JOIN sys.foreign_keys fk ON fk.parent_object_id = o.id AND fk.parent_object_id != fk.referenced_object_id
    INNER JOIN cteTables cte ON fk.referenced_object_id = cte.TableID
    WHERE o.type = 'U' 
),
cteFinal as
(
    SELECT DISTINCT 
          t.ordinal, 
          t.tableName
    FROM cteTables t 
    INNER JOIN
    ( 
       SELECT 
          tableName, 
          MAX( ordinal ) AS ordinal
       FROM cteTables
       GROUP BY tableName
    ) cte ON t.tableName = cte.tableName AND t.ordinal = cte.ordinal
    WHERE t.tableName not in
    (
       /* i need to eliminate tables like sysdiagrams, dtproperties, ... */
       SELECT QUOTENAME( [schema] ) + '.' + QUOTENAME( name )AS tableName
        FROM( 
             SELECT SCHEMA_NAME( t0.schema_id )AS [schema] , 
                   name , 
                   (
                    SELECT MAJOR_ID
                    FROM SYS.EXTENDED_PROPERTIES WITH ( NOLOCK )
                    WHERE 
                       MAJOR_ID = t0.object_id AND 
                       MINOR_ID = 0 AND 
                       CLASS = 1 AND 
                       NAME = N'MICROSOFT_DATABASE_TOOLS_SUPPORT'
                    ) AS MS_ID
               FROM sys.tables t0 )t1
        WHERE MS_ID IS NOT NULL
    )
 )
SELECT 
    ordinal,
    tableName 
FROM cteFinal    
ORDER BY
    ROW_NUMBER() OVER(ORDER BY ordinal , tableName)

 

Eseguendo il codice T-SQL sul database visualizzato in figura:

image

 

A questo punto, se volessi popolare il database (con dati dummy, di test), posso utilizzare la procedura già postata qui sfruttando lo stesso ordine riportato dal codice mandato in esecuzione.

Ad esempio:

 EXEC dbo.up_insertDummyDataWithFK @schemaName = 'dbo', @tableName = 'invoiceHeader' , @numberOfRows = 2;
EXEC dbo.up_insertDummyDataWithFK @schemaName = 'dbo', @tableName = 'product' , @numberOfRows = 2;
EXEC dbo.up_insertDummyDataWithFK @schemaName = 'dbo', @tableName = 'invoiceRow' , @numberOfRows = 2;
EXEC dbo.up_insertDummyDataWithFK @schemaName = 'dbo', @tableName = 'invoiceRowDetail' , @numberOfRows = 2;

 

Vedo i dati:

 select * from [dbo].[invoiceHeader]
select * from [dbo].[product]
select * from [dbo].[invoiceRow]
select * from [dbo].[invoiceRowDetail]

image

 

Se volessi, invece, svuotare le tabelle dovrei utilizzare l’ordine inverso (quindi dall’ultima figlia, fino al padre).

Ad esempio:

 delete from [dbo].[invoiceRowDetail]
delete from [dbo].[invoiceRow]
delete from [dbo].[product]
delete from [dbo].[invoiceHeader]