Ogni tanto mi capita di rispondere sui ng ad una domanda piuttosto ricorrente che riguarda le viste.
Diversi utenti lamentano il fatto che dopo aver apportato delle modifiche strutturali alle tabelle referenziate dalle loro viste, queste ultime restituiscono valori "sballati", oppure se la query utilizzata nella definizione della vista era la classica SELECT *, non vengono restituite le nuove colonne aggiunte successivamente.
Come mai si verifica questo disallineamento?
Prima di iniziare voglio sottolineare che non si tratta di un bug o una anomalia, ma è previsto dallo standard ANSI SQL 92.
Cerchiamo di analizzare meglio la faccenda.
Il comando TSQL
CREATE VIEW permette di definire una tabella virtuale che rappresenta in modo alternativo i dati di una o più tabelle base.
Questa tabella virtuale prende il nome di Vista.
Ma che succede dietro le quinte quando andiamo a definire una vista?
SQL Server andrà a popolare alcune tabelle di sistema con i metadati della vista stessa.
In particolare verrà popolata la tabella
sysobjects con il nome della vista,
syscolumns con le colonne restituite dalla query,
sysdepends con i riferimenti alle tabelle utilizzate nella query e
syscomments con l'intero comando
CREATE VIEW.
Queste informazioni sono memorizzate staticamente, nel senso che non rifletteranno le eventuali modifiche effettuate sulle tabelle base referenziate dalla vista.
Se si modificano le tabelle base, le viste andranno a restituire risultati sbagliati, le colonne saranno disallineate oppure mancheranno del tutto quelle aggiunte successivamente alla definizione della vista.
Come possiamo ovviare a questo inconveniente?
Possiamo seguire 3 strade:
1) Eliminare le viste influenzate dalla modifica e ricrearle successivamente;
2) Modificare le viste tramite il comando
ALTER VIEW;
3) Utilizzare la stored procedure estesa di sistema
sp_refreshview.
I primi due metodi richiedono necessariamente di conoscere a priori i metadati delle viste e spesso questa operazione è poco agevole se non impossibile (nel caso il cui la vista sia stata criptata tramite la clausola
WITH ENCRYPTION).
La stored procedure estesa di sistema
sp_refreshview ci semplifica di molto la vita: è sufficiente eseguirla specificando come argomento il nome della vista che si intende "rinfrescare".
Ho preparato un piccolo esempio che illustra come utilizzare la
sp_refreshview:
USE tempdb
GO
/* Definisco la tabella dbo.Students */
CREATE TABLE dbo.Students(
StudentID int NOT NULL IDENTITY PRIMARY KEY,
FirstName varchar(10) NOT NULL,
LastName varchar(10) NOT NULL
)
GO
/* La popolo */
SET NOCOUNT ON
INSERT dbo.Students VALUES('Lorenzo', 'Benaglia')
INSERT dbo.Students VALUES('Luca', 'Bianchi')
INSERT dbo.Students VALUES('Andrea', 'Montanari')
INSERT dbo.Students VALUES('Gianluca', 'Hotz')
SET NOCOUNT OFF
GO
/* Definisco la vista dbo.vw_GetStudentsFirstName */
CREATE VIEW dbo.vw_GetStudentsFirstName
WITH ENCRYPTION
AS
SELECT FirstName
FROM dbo.Students
GO
/* Definisco la vista dbo.vw_GetStudents */
CREATE VIEW dbo.vw_GetStudents
AS
SELECT *
FROM dbo.Students
GO
/* Vediamo il data type della colonna FirstName
** della tabella e delle due viste
*/
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE COLUMN_NAME = N'FirstName'
AND TABLE_SCHEMA = N'dbo'
AND TABLE_NAME IN(N'Students', N'vw_GetStudentsFirstName', N'vw_GetStudents')
GO
/* Output:
TABLE_NAME COLUMN_NAME DATA_TYPE
------------------------- ------------ ----------
Students FirstName varchar
vw_GetStudents FirstName varchar
vw_GetStudentsFirstName FirstName varchar
(3 row(s) affected)
*/
/* Modifico il data type della colonna FirstName
** nella tabella dbo.Students
*/
ALTER TABLE dbo.Students
ALTER COLUMN FirstName nvarchar(10) NOT NULL
GO
/* Vediamo il data type della colonna FirstName
** della tabella e delle due viste
*/
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE COLUMN_NAME = N'FirstName'
AND TABLE_SCHEMA = N'dbo'
AND TABLE_NAME IN(N'Students', N'vw_GetStudentsFirstName', N'vw_GetStudents')
GO
/* Output:
TABLE_NAME COLUMN_NAME DATA_TYPE
------------------------- ------------ ----------
Students FirstName nvarchar
vw_GetStudents FirstName varchar
vw_GetStudentsFirstName FirstName varchar
(3 row(s) affected)
*/
/* Come si può vedere la modifica del data type nella tabella base non è stata
** riflessa nei metadati delle viste.
** OK, diamo loro una bella "rinfrescata" :-D
*/
EXEC sp_refreshview [dbo.vw_GetStudentsFirstName]
EXEC sp_refreshview [dbo.vw_GetStudents]
GO
/* Vediamo il data type della colonna FirstName
** della tabella e delle due viste
*/
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE COLUMN_NAME = N'FirstName'
AND TABLE_SCHEMA = N'dbo'
AND TABLE_NAME IN(N'Students', N'vw_GetStudentsFirstName', N'vw_GetStudents')
GO
/* Output:
TABLE_NAME COLUMN_NAME DATA_TYPE
------------------------- ------------ ----------
Students FirstName nvarchar
vw_GetStudents FirstName nvarchar
vw_GetStudentsFirstName FirstName nvarchar
(3 row(s) affected)
*/
/* Nel caso in cui le viste da aggiornare siano tante,
** è possibile generare i comandi di aggiornamento
** utilizzando le information_schema views
*/
SELECT N'EXEC sp_refreshview [' +
TABLE_SCHEMA + N'.' +
TABLE_NAME + N']' AS RefreshCommand
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = N'VIEW'
AND TABLE_NAME NOT LIKE 'sys%'
GO
/* Output:
RefreshCommand
--------------------------------------------------
EXEC sp_refreshview [dbo.vw_GetStudents]
EXEC sp_refreshview [dbo.vw_GetStudentsFirstName]
(2 row(s) affected)
*/
/* Pulizia */
DROP VIEW dbo.vw_GetStudentsFirstName, dbo.vw_GetStudents
DROP TABLE dbo.Students
Teoricamente una volta che è stata definita una vista o una user defined function (udf), non ha senso modificare gli oggetti referenziati da esse (come le tabelle base) dato che alcune modifiche potrebbero causare i malfunzionamenti di cui abbiamo appena discusso.
SQL Server 2000 ci permette di stabilire una sorta di contratto (chiamato bind) mediante l'attributo
SCHEMABINDING che lega una vista o una udf agli oggetti referenziati.
Ogni operazione sugli oggetti referenziati che comporterà un potenziale malfunzionamento agli oggetti referenzianti verrà inibito, scatenando una eccezione.
Per capire meglio il funzionamento dell'attributo
SCHEMABINDING guardiamo il seguente esempio:
USE tempdb
GO
/* Definisco la tabella dbo.Students */
CREATE TABLE dbo.Students(
StudentID int NOT NULL IDENTITY PRIMARY KEY,
FirstName varchar(10) NOT NULL,
LastName varchar(10) NOT NULL
)
GO
/* La popolo */
SET NOCOUNT ON
INSERT dbo.Students VALUES('Lorenzo', 'Benaglia')
INSERT dbo.Students VALUES('Luca', 'Bianchi')
INSERT dbo.Students VALUES('Andrea', 'Montanari')
INSERT dbo.Students VALUES('Gianluca', 'Hotz')
SET NOCOUNT OFF
GO
/* Definisco la vista dbo.vw_GetStudentsFirstName */
CREATE VIEW dbo.vw_GetStudentsFirstName
WITH SCHEMABINDING
AS
SELECT FirstName
FROM dbo.Students
GO
/* 1) Provo ad eliminare la tabella dbo.Students referenziata dalla vista */
DROP TABLE dbo.Students
GO
/* Output:
Server: Msg 3729, Level 16, State 1, Line 1
Cannot DROP TABLE 'dbo.Students' because it is being referenced by object 'vw_GetStudentsFirstName'.
*/
/* 2) Provo a modificare il data type della colonna FirstName (utilizzato dalla vista) */
ALTER TABLE dbo.Students
ALTER COLUMN FirstName varchar(15) NOT NULL
GO
/* Output:
Server: Msg 5074, Level 16, State 3, Line 1
The object 'vw_GetStudentsFirstName' is dependent on column 'FirstName'.
Server: Msg 4922, Level 16, State 1, Line 1
ALTER TABLE ALTER COLUMN FirstName failed because one or more objects access this column.
*/
/* 2.1) La cosa interessante è che l'eccezione viene scatenata anche se per assurdo
** andiamo a modificare la colonna FirstName specificando esattamente lo stesso data type,
** la stessa lunghezza in bytes e la stessa nullability utilizzate nella definizione della tabella
*/
ALTER TABLE dbo.Students
ALTER COLUMN FirstName varchar(10) NOT NULL
GO
/* Output:
Server: Msg 5074, Level 16, State 3, Line 1
The object 'vw_GetStudentsFirstName' is dependent on column 'FirstName'.
Server: Msg 4922, Level 16, State 1, Line 1
ALTER TABLE ALTER COLUMN FirstName failed because one or more objects access this column.
*/
/* 3) Provo a modificare il data type della colonna LastName (non utilizzata dalla vista) */
ALTER TABLE dbo.Students
ALTER COLUMN LastName varchar(15) NOT NULL
GO
/* Output:
The command(s) completed successfully.
*/
/* Pulizia */
DROP VIEW dbo.vw_GetStudentsFirstName
DROP TABLE dbo.Students
Il mio suggerimento è quello di utilizzare l'attributo
SCHEMABINDING una volta che la struttura del database è stata verificata e consolidata, in modo da evitare potenziali malfunzionamente spesso difficili da rintracciare.