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, ordinatosecondo il naturale ordine di relazione: prima le tabelle padre e quella senza relazioni, dopo le tabelle figlie (o figlie di figlie):
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 DISTINCTQUOTENAME(OBJECT_SCHEMA_NAME( o.id )) + '.' + QUOTENAME(OBJECT_NAME( o.id )) AS tableName, o.id AS TableID , 0 AS ordinalFROM dbo.sysobjects o INNER JOIN sys.all_columns c ON o.ID = c.object_idWHERE 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 ordinalFROM dbo.sysobjects o INNER JOIN sys.foreign_keys fk ON fk.parent_object_id = o.id AND fk.parent_object_id != fk.referenced_object_idINNER JOIN cteTables cte ON fk.referenced_object_id = cte.TableIDWHERE o.type = 'U' ),cteFinal as(SELECT DISTINCT t.ordinal, t.tableNameFROM cteTables t INNER JOIN ( SELECT tableName, MAX( ordinal ) AS ordinalFROM cteTablesGROUP BY tableName) cte ON t.tableName = cte.tableName AND t.ordinal = cte.ordinalWHERE t.tableName not in (/* i need to eliminate tables like sysdiagrams, dtproperties, ... */SELECT QUOTENAME( [schema] ) + '.' + QUOTENAME( name )AS tableNameFROM( SELECT SCHEMA_NAME( t0.schema_id )AS [schema] , name , (SELECT MAJOR_IDFROM 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_IDFROM sys.tables t0 )t1WHERE MS_ID IS NOT NULL ) )SELECT ordinal,tableName FROM cteFinal ORDER BYROW_NUMBER() OVER(ORDER BY ordinal , tableName)
Eseguendo il codice T-SQL sul database visualizzato in figura:
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]
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]