VN:F [1.7.9_1023]
Rating: 0.0/5 (0 votes cast)

Pilot

Umiejętność korzystania z metadanych w celu skryptowania obiektów w bazach danych (tabel, procedur, itd.) przydaje się w życiu administratora i programisty baz danych. Sam wykorzystuję skryptowanie do cyklicznego tworzenia kopii kodu T-SQL obiektów w bazach danych (na wypadek, gdyby akurat okazało się, że w bazie uszkodzeniu uległ któryś z obiektów przechowujących metadane). Zapytasz, czemu nie użyć do tego mechanizmu skryptowania oferowanego przez Management Studio? Otóż skryptowanie z użyciem własnego kodu T-SQL daje mi więcej swobody, a ponadto skryptowanie w Management Studio ma swoje wady (patrz tutaj: [EN] SQL Server – Don’t (sp_)rename your database objects) i na dodatek często występują spore problemy w przypadku, gdy baza danych zawiera sporo obiektów (jakoś listowanie pokaźnej listy obiektów nie wychodzi narzędziu Management Studio – często aplikacja się zawiesza albo odpowiada wyjątkiem klasy OutOfMemoryException – patrz tutaj: Dziennik pokładowy MSSQL 2008 (wpis 0×0011) – Problem z Generate Script…).

Dlatego postanowiłem napocząć na blogu nową serię wpisów poświęconych tworzeniu własnego kodu T-SQL do skryptowania. Zaczynam od skryptowania obiektów proceduralnych – funkcji, procedur, widoków i wyzwalaczy. Potem spróbuję pokazać, jak można skryptować typy danych użytkownika, więzy, obiekty CLR i tabele. Tam, gdzie ilość kodu nie będzie wykraczała poza możliwości percepcji, będę się starał pokazać pełne rozwiązanie, ale na przykład w przypadku tabel już wiem, że nie będę podawał pełnego rozwiązania, a tylko podsunę kawałek kodu i parę pomysłów na dobry początek :-)

Kod będę tworzył z myślą o skryptowaniu w SQL Server 2008 i SQL Server 2008 R2. Przy odrobinie wysiłku kod będzie do zaadoptowania na potrzeby SQL Server 2005.

Funkcje, procedury, widoki

Zadanie: zeskryptować wszystkie funkcje, procedury i widoki w wybranej bazie danych.

Rozwiązanie:

USE AdventureWorks2008R2;
GO
SET NOCOUNT ON;
DECLARE @SQL TABLE (
  LineId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
  Line nvarchar(4000) NOT NULL
);
DECLARE
  @schema_name sysname,
  @object_name sysname,
  @type_desc nvarchar(60),
  @type char(2),
  @definition nvarchar(max),
  @pindex int;
DECLARE CursorObjects CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
  SELECT
    SCHEMA_NAME(o.[schema_id]),
    o.name,
    o.type_desc COLLATE database_default,
    o.type COLLATE database_default,
    m.definition
  FROM sys.objects AS o
  INNER JOIN sys.sql_modules AS m
  ON m.[object_id] = o.[object_id]
  WHERE o.type IN ('FN', 'IF', 'P', 'TF', 'V')
  AND o.is_ms_shipped = 0
  ORDER BY SCHEMA_NAME(o.[schema_id]), o.name;
OPEN CursorObjects;
FETCH NEXT FROM CursorObjects
INTO
  @schema_name,
  @object_name,
  @type_desc,
  @type,
  @definition;
WHILE @@FETCH_STATUS = 0 BEGIN
  SET @pindex = 0;

  -- generujemy PRINT
  INSERT INTO @SQL (Line)
  SELECT 'PRINT N''-- ' + @type_desc + ': ' +
  REPLACE(
    QUOTENAME(@schema_name) + '.' +
    QUOTENAME(@object_name), '''', ''''''
  ) + '''';

  IF @definition IS NOT NULL
  BEGIN
    -- generujemy DROP
    INSERT INTO @SQL (Line)
    SELECT 'IF OBJECT_ID(N''' +
      REPLACE(
        QUOTENAME(@schema_name) + '.' +
        QUOTENAME(@object_name), '''', ''''''
      ) + ''', N''' + (@type) + ''') IS NOT NULL';
    INSERT INTO @SQL (Line) SELECT '  DROP ' +
      CASE
        WHEN @type = 'P' THEN 'PROCEDURE'
        WHEN @type IN ('FN', 'IF', 'TF') THEN 'FUNCTION'
        ELSE 'VIEW'
      END + ' ' +
      QUOTENAME(@schema_name) + '.' +
      QUOTENAME(@object_name) + ';';

    -- poprzedzamy definicję słowem GO  
    INSERT INTO @SQL (Line) SELECT 'GO';

    -- kroimy definicję na poszczególne wiersze
    SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) + '%', @definition);
    WHILE @pindex > 0 BEGIN
      INSERT INTO @SQL(Line)
      SELECT REPLACE(SUBSTRING(@definition, 1, @pindex), CHAR(13), '');
      SET @definition = SUBSTRING(@definition, @pindex + 2, LEN(@definition));
      SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) +'%', @definition);
    END;
  END;
  ELSE
  BEGIN
    INSERT INTO @SQL (Line)
    SELECT 'PRINT N''-- *** This object is encrypted ***''';
  END;

  -- kończymy definicję słowem GO
  INSERT INTO @SQL (Line) SELECT 'GO';  

  FETCH NEXT FROM CursorObjects
  INTO
    @schema_name,
    @object_name,
    @type_desc,
    @type,
    @definition;
END;
CLOSE CursorObjects;
DEALLOCATE CursorObjects;
SELECT Line AS ' ' FROM @SQL ORDER BY LineId;
GO

Fragment wyniku:

image

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • definicje obiektów są trzymane w widoku sys.sql_modules w kolumnie definition, wyjątek stanowią obiekty z opcją WITH ENCRYPTION użytą w definicji (takie obiekty są “szyfrowane”),
  • listę obiektów stworzonych przez użytkowników w bazie danych zwraca widok sys.objects (ale jest też widok sys.all_objects, w którym dodatkowo są zawarte metadane obiektów systemowych),
  • fragmenty generowanego skryptu T-SQL są dodawane w kursorze (iteracja = definicja jednego obiektu) do zmiennej tabelarycznej @SQL (linie są numerowane autonumeracją w kolumnie LineId, zakładam maksymalną długość jednej linii kodu w obiekcie na 4000 znaków),
  • zapytanie filtruje obiekty wg typu – wybiera funkcje trzech rodzajów, procedury i widoki, jest też filtr odrzucający ewentualne obiekty systemowe (kolumna is_ms_shipped w widoku sys.objects),
  • przed definicją każdego obiektu dorzucam kod wykrywający obecność obiektu w bazie danych i usuwający obiekt w przypadku, gdy takowy istnieje,
  • kolumny type i type_desc z widoku sys.objects mają na sztywno przypisane collation, stąd użycie funkcji COLLATE (inaczej nie dałoby się sklejać tych kolumn z tekstem, gdy collation bazy jest inne niż collation wspomnianych kolumn),
  • operuję zawsze dwuczłonowymi nazwami obiektów (dzięki temu nazwa jest jednoznaczna w kontekście bazy danych) – do uzyskania nazwy schematu obiektu wykorzystuję funkcję SCHEMA_NAME,
  • funkcja REPLACE jest przeze mnie użyta do “wyescape’owania” apostrofów w nazwach obiektów (swoją drogą, jak ktoś używa niestandardowych identyfikatorów zawierających dziwne znaczki i/lub słowa kluczowe, sam prosi się o kłopoty),
  • funkcja QUOTENAME pozwala otoczyć identyfikatory nawiasami kwadratowymi – zabezpieczam się przed niestandardowymi identyfikatorami.

Wyzwalacze na poziomie bazy

Zadanie: zeskryptować wszystkie wyzwalacze w wybranej bazie danych.

Rozwiązanie:

-- wyzwalacze DML i DDL (ON DATABASE)
USE AdventureWorks2008R2;
GO
SET NOCOUNT ON;
DECLARE @SQL TABLE (
  LineId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
  Line nvarchar(4000) NOT NULL
);
DECLARE
  @schema_name sysname,
  @object_name sysname,
  @type_desc nvarchar(60),
  @parent_class int,
  @definition nvarchar(max),
  @pindex int;
DECLARE CursorObjects CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
  SELECT
    OBJECT_SCHEMA_NAME(t.[object_id]),
    t.name,
    t.type_desc COLLATE database_default,
    t.parent_class,
    m.definition
  FROM sys.triggers AS t
  INNER JOIN sys.sql_modules AS m
  ON m.[object_id] = t.[object_id]
  WHERE t.is_ms_shipped = 0
  ORDER BY OBJECT_SCHEMA_NAME(t.[object_id]), t.name;
OPEN CursorObjects;
FETCH NEXT FROM CursorObjects
INTO
  @schema_name,
  @object_name,
  @type_desc,
  @parent_class,
  @definition;
WHILE @@FETCH_STATUS = 0 BEGIN
  SET @pindex = 0;

  -- generujemy PRINT
  INSERT INTO @SQL (Line)
  SELECT 'PRINT N''-- ' + @type_desc + ': ' +
  REPLACE(
    ISNULL(QUOTENAME(@schema_name) + '.', '') +
    QUOTENAME(@object_name), '''', ''''''
  ) + ''';';

  IF @definition IS NOT NULL
  BEGIN
    -- generujemy DROP
    INSERT INTO @SQL (Line)
    SELECT 'IF EXISTS (SELECT * FROM sys.triggers WHERE name = N''' +
      REPLACE(
        ISNULL(QUOTENAME(@schema_name) + '.', '') +
        QUOTENAME(@object_name),
        '''',
        ''''''
      ) + ''')';
    INSERT INTO @SQL (Line)
    SELECT '  DROP TRIGGER ' +
      ISNULL(QUOTENAME(@schema_name) + '.', '') +
      QUOTENAME(@object_name) +
      CASE WHEN @parent_class = 0 THEN ' ON DATABASE' ELSE '' END + ';';

    -- poprzedzamy definicję słowem GO  
    INSERT INTO @SQL (Line) SELECT 'GO';

    -- kroimy definicję na poszczególne wiersze
    SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) + '%', @definition);
    WHILE @pindex > 0 BEGIN
      INSERT INTO @SQL(Line)
      SELECT REPLACE(SUBSTRING(@definition, 1, @pindex), CHAR(13), '');
      SET @definition = SUBSTRING(@definition, @pindex + 2, LEN(@definition));
      SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) +'%', @definition);
    END;
  END;
  ELSE
  BEGIN
    INSERT INTO @SQL (Line)
    SELECT 'PRINT N''-- *** This object is encrypted ***''';
  END;

  -- kończymy definicję słowem GO
  INSERT INTO @SQL (Line) SELECT 'GO';  

  FETCH NEXT FROM CursorObjects
  INTO
    @schema_name,
    @object_name,
    @type_desc,
    @parent_class,
    @definition;
END;
CLOSE CursorObjects;
DEALLOCATE CursorObjects;
SELECT Line AS ' ' FROM @SQL ORDER BY LineId;
GO

Fragment wyniku:

image

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • listę wyzwalaczy w bazie danych zwraca widok sys.triggers,
  • fragmenty generowanego skryptu T-SQL są dodawane w kursorze (iteracja = definicja jednego obiektu) do zmiennej tabelarycznej @SQL (linie są numerowane autonumeracją w kolumnie LineId, zakładam maksymalną długość jednej linii kodu w obiekcie na 4000 znaków),
  • drobnym wyzwaniem było rozróżnienie wyzwalaczy DML i DDL – każdy z tych typów wyzwalaczy wymaga nieco innego kodu przy usuwaniu, posłużyłem się kolumna parent_class z widoku sys.triggers,
  • ponieważ w widoku sys.triggers nie ma kolumny schema_id, musiałem użyć funkcji OBJECT_SCHEMA_NAME do wyłuskania nazwy schematu,
  • przy okazji taka obserwacja – wyzwalacz DML można utworzyć tylko w tym samym schemacie, w którym znajduje się nadrzędna tabela/widok.

Wyzwalacze na poziomie serwera

Zadanie: zeskryptować wszystkie wyzwalacze DDL na poziomie serwera.

Rozwiązanie:

USE master;
GO
SET NOCOUNT ON;
DECLARE @SQL TABLE (
  LineId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
  Line nvarchar(4000) NOT NULL
);
DECLARE
  @object_name sysname,
  @type_desc nvarchar(60),
  @definition nvarchar(max),
  @pindex int;
DECLARE CursorObjects CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
  SELECT
    t.name,
    t.type_desc COLLATE database_default,
    m.definition
  FROM sys.server_triggers AS t
  INNER JOIN sys.server_sql_modules AS m
  ON m.[object_id] = t.[object_id]
  WHERE t.is_ms_shipped = 0
  ORDER BY t.name;
OPEN CursorObjects;
FETCH NEXT FROM CursorObjects
INTO
  @object_name,
  @type_desc,
  @definition;
WHILE @@FETCH_STATUS = 0 BEGIN
  SET @pindex = 0;

  -- generujemy PRINT
  INSERT INTO @SQL (Line)
  SELECT 'PRINT N''-- ' + @type_desc + ': ' +
  REPLACE(
    QUOTENAME(@object_name), '''', ''''''
  ) + ''';';

  IF @definition IS NOT NULL
  BEGIN
    -- generujemy DROP
    INSERT INTO @SQL (Line)
    SELECT 'IF EXISTS (SELECT * FROM sys.server_triggers WHERE name = N''' +
      REPLACE(
        @object_name, '''', ''''''
      ) + ''')';
    INSERT INTO @SQL (Line)
    SELECT '  DROP TRIGGER ' +
      QUOTENAME(@object_name) + ' ON ALL SERVER;'

    -- poprzedzamy definicję słowem GO  
    INSERT INTO @SQL (Line) SELECT 'GO';

    -- kroimy definicję na poszczególne wiersze
    SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) + '%', @definition);
    WHILE @pindex > 0 BEGIN
      INSERT INTO @SQL(Line)
      SELECT REPLACE(SUBSTRING(@definition, 1, @pindex), CHAR(13), '');
      SET @definition = SUBSTRING(@definition, @pindex + 2, LEN(@definition));
      SET @pindex = PATINDEX('%' + CHAR(13) + CHAR(10) +'%', @definition);
    END;
  END;
  ELSE
  BEGIN
    INSERT INTO @SQL (Line)
    SELECT 'PRINT N''-- *** This object is encrypted ***''';
  END;

  -- kończymy definicję słowem GO
  INSERT INTO @SQL (Line) SELECT 'GO';  

  FETCH NEXT FROM CursorObjects
  INTO
    @object_name,
    @type_desc,
    @definition;
END;
CLOSE CursorObjects;
DEALLOCATE CursorObjects;
SELECT Line AS ' ' FROM @SQL ORDER BY LineId;
GO

Fragment wyniku:

image

Uwagi: w Management Studio ustaw Results to Text, sugeruję też zwiększenie maksymalnej długości tekstu w jednej kolumnie zwracanej w wynikach zapytań (w menu głównym kliknij Tools – Options – Query Results – SQL Server – Results to Text i w polu Maximum number of characters displayed in each column wpisz 8192, a następnie uruchom ponownie Management Studio).

Komentarz do kodu:

  • listę wyzwalaczy na poziomie serwera zwraca widok sys.server_triggers,
  • definicje widoków na poziomie serwera znajdują się w kolumnie definition widoku sys.server_sql_modules,
  • fragmenty generowanego skryptu T-SQL są dodawane w kursorze (iteracja = definicja jednego obiektu) do zmiennej tabelarycznej @SQL (linie są numerowane autonumeracją w kolumnie LineId, zakładam maksymalną długość jednej linii kodu w obiekcie na 4000 znaków),
  • tu było bodaj najłatwiej, ponieważ wyzwalacze na poziomie serwera nie mają przypisanego schematu.

Słowo podsumowania

Skryptowanie pojedynczego obiektu proceduralnego to nie problem. SQL Server oferuje procedurę sp_helptext oraz funkcję OBJECT_DEFINITION. Ale jeżeli chcemy hurtowo skryptować obiekty, podany w powyżej kod może okazać się przydatny. Można na jego podstawie zbudować własne procedury do skryptowania i używać ich na codzień. Tu polecam wpis z mojego starego bloga:  [PL][util] Piszemy narzędzia w T-SQL cz. 1 – sp_getcolumns.

Zachęcam do zabawy z metadanymi SQL Servera. Można się dzięki takiej zabawie sporo nauczyć. I oczywiście zachęcam do komentowania tego wpisu. Może masz jakieś uwagi, coś nie gra w podanym tu kodzie albo masz lepszy pomysł na realizację takich zadań.

[EDYCJA] To już druga wersja tego wpisu. W pierwszej kod T-SQL nie bazował na kursorach, a definicja każdego obiektu była zwracana w postaci jednej wartości typu nvarchar(max), co powodowało problem w przypadku, gdy definicja obiektu była dłuższa niż 8192 znaki (a nawet mniej, gdyż Management Studio domyślnie wyświetla w kolumnie bodajże do 255 znaków). Druga wersja powstała dzięki czujności (po raz kolejny) Krzysia Stachyry. Krzysiu, wielki szacunek za cierpliwe testowanie moich wypocin! Tym razem powinno być ciut lepiej :-) [/EDYCJA]