SQL Server w logistyce

Klonowanie wiersza konfiguracji w T-SQL: dynamiczna lista kolumn i bezpieczniki THROW

Nowy wiersz konfiguracji powstaje jako kopia wiersza wzorcowego, a lista kolumn pochodzi z sys.columns. Bezpieczniki THROW zatrzymują skrypt, a przebieg próbny kończy się ROLLBACK.

sql.server.net.pl/sql-server/
Programiści przy monitorach z kodem w otwartej przestrzeni biurowej
Programiści przy monitorach z kodem w otwartej przestrzeni biurowej
W skrócie
  • Wzorzec: nowy wiersz to kopia wiersza wzorcowego, w której nazwa roli i etykieta pochodzą z parametrów, a lista kolumn z sys.columns.
  • Pominięte kolumny: cztery rodzaje kolumn, których nie wolno wstawiać albo które mają dostać wartość domyślną, wymienia tabela.
  • Bezpieczniki: THROW 50100-50103 zatrzymują skrypt, a drugie uruchomienie wypisuje komunikat i niczego nie zmienia.
  • Próba: parametr @Zatwierdz = 0 kończy transakcję przez ROLLBACK, a numer identity i tak zostaje zużyty.

Klonowanie wiersza w T-SQL z INSERT SELECT ma sens, gdy nowa rola ma dostać konfigurację taką jak istniejąca, a schemat tabeli może się zmienić. W przykładzie wdrożeniowiec ma skonfigurowaną rolę MAG1 i potrzebuje takiej samej konfiguracji dla roli MAG2. Skrypt sam składa listę kolumn z katalogu, więc dodanie kolumny do tabeli nie wymaga zmiany skryptu.

Nowy wiersz konfiguracji najprościej zrobić jako kopię wiersza wzorcowego. Listę kolumn pobieramy z sys.columns, pomijając kolumny, których nie wolno wstawiać. Skrypt zatrzymuje się przez THROW, gdy brakuje wzorca albo podmiana nie trafiła, drugie uruchomienie niczego nie zmienia, a przebieg próbny kończy się ROLLBACK.

To wzorzec z nazwami przykładowymi. Sprawdziliśmy go na SQL Server 2022 przy czterech poziomach zgodności z zakresu od 110 do 160 i wynik był za każdym razem ten sam. Przebieg próbny wykonujemy na bazie, na której wolno uruchamiać skrypt.

Nowy wiersz konfiguracji powstaje z kopii wiersza wzorcowego

Tabela konfiguracji roli ma dziesięć kolumn, wymienionych w pierwszym bloku poniżej. Wśród nich są numer z IDENTITY i kolumna obliczana, wersja ma typ rowversion, a kolumna Utworzono wartość domyślną SYSDATETIME().

Indeks unikatowy filtrowany obejmuje parę nazwa roli i kod pozycji dla wierszy aktywnych. Dane próbne to trzy wiersze roli MAG1 i jeden wiersz roli ROLA3. Sam indeks filtrowany opisuje artykuł o indeksie filtrowanym i miękkim usuwaniu, a tutaj jest tylko tłem jednej z pułapek.

CREATE TABLE dbo.KonfiguracjaRoli (
    Id               int            IDENTITY(1,1) NOT NULL PRIMARY KEY,
    NazwaRoli             varchar(20)    NOT NULL,
    KodPozycji              varchar(30)    NOT NULL,
    Etykieta            nvarchar(100)  NOT NULL,
    Porzadek        int            NOT NULL,
    [Opis dodatkowy] nvarchar(200)  NULL,
    Aktywna          bit            NOT NULL CONSTRAINT DF_Konf_Akt DEFAULT (1),
    Utworzono        datetime2(0)   NOT NULL CONSTRAINT DF_Konf_Utw DEFAULT (SYSDATETIME()),
    Wersja           rowversion     NOT NULL,
    KluczPozycji            AS (NazwaRoli + '/' + KodPozycji)
);
CREATE UNIQUE INDEX UX_KonfiguracjaRoli ON dbo.KonfiguracjaRoli (NazwaRoli, KodPozycji) WHERE Aktywna = 1;
GO
INSERT dbo.KonfiguracjaRoli (NazwaRoli, KodPozycji, Etykieta, Porzadek, [Opis dodatkowy], Utworzono) VALUES
 ('MAG1',  'PRZYJECIE',  N'Przyjęcie',  10, N'ekran przyjęcia', '2026-01-05'),
 ('MAG1',  'WYDANIE',    N'Wydanie',    20, NULL,               '2026-01-05'),
 ('MAG1',  'SPIS',       N'Spis',       30, N'tylko kierownik', '2026-01-05'),
 ('ROLA3', 'ZLECENIE',   N'Zlecenie',   10, NULL,               '2026-01-05');

Przykład ilustracyjny. Wszystkie nazwy w przykładzie są wymyślone. GO rozdziela partie poleceń.

Lista kolumn pochodzi z katalogu sys.columns

Zamiast wpisywać kolumny ręcznie, czytamy je z sys.columns dla tabeli, w kolejności column_id. Pomijamy cztery rodzaje kolumn, wymienione w tabeli poniżej. Każda nazwa przechodzi przez QUOTENAME.

Rodzaje kolumn i sposób ich obsługi przy klonowaniu wiersza
Rodzaj kolumnyCzy kopiowana z wzorcaSkąd wartość nowego wiersza
identitynienadaje silnik
obliczananieprzeliczana z nowych wartości
rowversionnienowa wersja
Utworzono (wartość domyślna, pominięta jawnie w skrypcie)niewartość domyślna
porządek i opis dodatkowytakz wiersza wzorcowego
kod pozycji i aktywnatakz wiersza wzorcowego (także Aktywna, choć ma wartość domyślną)
nazwa roli i etykietaniez parametrów skryptu

Przykład. Sposób obsługi kolumn tabeli konfiguracji z poprzedniego bloku.

Z jednego zapytania pomocniczego powstają dwie listy: kolumny docelowe i wartości. W liście wartości nazwa roli i etykieta są zastąpione parametrami @NowaRola i @NowaEtykieta, a pozostałe kolumny kopiujemy z wiersza wzorcowego.

Obie listy składa FOR XML PATH('') z .value('.', 'nvarchar(max)'). Sprawdziliśmy to na SQL Server 2022 przy poziomach zgodności od 110 do 160, a starszych silników nie sprawdzaliśmy. Funkcja STRING_AGG z WITHIN GROUP (ORDER BY column_id), dostępna od SQL Server 2017 (według dokumentacji), zbudowała listę w tej samej postaci. Nad wartościami nvarchar(4000) kończy się ona błędem 9829, gdy wynik przekracza 8000 bajtów, więc dla długich list rzutujemy wartości na nvarchar(max).

INSERT SELECT przechodzi przez sp_executesql z parametrami

Nazwę tabeli do polecenia bierzemy z katalogu: QUOTENAME(OBJECT_SCHEMA_NAME(id)) + '.' + QUOTENAME(OBJECT_NAME(id)). Wartości przekazujemy jako parametry sp_executesql, a nie sklejamy w tekst. QUOTENAME(N'Opis dodatkowy') daje [Opis dodatkowy], a QUOTENAME(N'a]b') daje [a]]b], więc nazwa z nawiasem nie rozbija polecenia.

SET XACT_ABORT ON;
DECLARE @Wzorzec int = 1, @NowaRola varchar(20) = 'MAG2', @NowaEtykieta nvarchar(100) = N'Przyjęcie (wariant B)', @Zatwierdz bit = 0;
DECLARE @id int = OBJECT_ID(N'dbo.KonfiguracjaRoli'), @tabela nvarchar(300), @kolumny nvarchar(max), @wartosci nvarchar(max), @sql nvarchar(max), @maxId int;
SET @tabela = QUOTENAME(OBJECT_SCHEMA_NAME(@id)) + N'.' + QUOTENAME(OBJECT_NAME(@id));
WITH k AS (
    SELECT c.column_id, QUOTENAME(c.name) AS Nazwa,
           CASE c.name WHEN N'NazwaRoli' THEN N'@NowaRola' WHEN N'Etykieta' THEN N'@NowaEtykieta' ELSE QUOTENAME(c.name) END AS Wartosc
    FROM sys.columns AS c
    WHERE c.object_id = @id AND c.is_identity = 0 AND c.is_computed = 0
      AND c.system_type_id <> 189 AND c.name <> N'Utworzono')
SELECT @kolumny  = STUFF((SELECT N', ' + Nazwa   FROM k ORDER BY column_id FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 2, N''),
       @wartosci = STUFF((SELECT N', ' + Wartosc FROM k ORDER BY column_id FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 2, N'');

Przykład ilustracyjny. Skrypt, część 1: lista kolumn i wartości. Obie części skryptu tworzą jedną partię poleceń, a @Zatwierdz = 0 oznacza przebieg próbny.

Schemat klonowania wiersza konfiguracji z dynamiczną listą kolumnSchemat: wiersz wzorcowy, lista kolumn z sys.columns, INSERT SELECT w transakcji, kontrola THROW i COMMIT albo ROLLBACK.Klonowanie wiersza konfiguracji: lista kolumn z katalogu i bezpieczniki THROWSkrypt zatrzymuje się głośno, gdy brakuje wzorca, i niczego nie zmienia, gdy kopia już istnieje.Wiersz wzorcowy (Id)IdNazwaRoliKodPozycjiEtykieta1MAG1PRZYJECIEPrzyjęcieLista kolumn z sys.columns (bez identity, obliczanych, rowversion)Kolumny docelowe[NazwaRoli][KodPozycji][Etykieta][Porzadek][Opis dodatkowy][Aktywna]Wartości@NowaRola[KodPozycji]@NowaEtykieta[Porzadek][Opis dodatkowy][Aktywna]Pominięte: Id (identity), KluczPozycji (obliczana), Wersja (rowversion)oraz Utworzono, która dostaje wartość domyślną.lista pusta: THROW 50100Czy wzorzec istnieje i czy kopia jużjest?brakbrak wzorca: THROW 50101jestkopia już jest: komunikat „Wierszjuż istnieje, pomijam.”, bez zmianwzorzec jest, kopii brakBEGIN TRAN, INSERT SELECT przez sp_executesqlwartości przekazane jako parametry, nazwa tabeli z katalogujeden nowy wiersz z nową rolą ietykietą?nieTHROW 50102 lub 50103tak@Zatwierdz = 1?takCOMMITnieROLLBACK, przebieg próbnyPrzebieg próbny kończy się komunikatem „Przebieg próbny: zmiany wycofane.”
Schemat: przebieg skryptu od wiersza wzorcowego do decyzji COMMIT albo ROLLBACK, z bezpiecznikami THROW po drodze.

Skrypt klonujący ma zatrzymać się głośno, gdy wzorca brakuje, i niczego nie zmieniać, gdy kopia już istnieje.

Bezpieczniki THROW zatrzymują skrypt, a drugie uruchomienie niczego nie zmienia

Cztery bezpieczniki mają własne numery. THROW 50100 zatrzymuje skrypt, gdy lista kolumn jest pusta, a 50101, gdy brak wiersza wzorcowego. THROW 50102 reaguje, gdy @@ROWCOUNT po INSERT jest różny od 1, a 50103, gdy po wstawieniu nie ma dokładnie jednego nowego wiersza z nową rolą i nową etykietą.

W sprawdzeniu wywołano 50101 (brak wzorca) i 50103 (wyzwalacz dopisał drugi wiersz; transakcja wycofana, stan bez zmian). Bezpieczniki 50100 i 50102 nie zostały uruchomione w przykładzie. Przy SET XACT_ABORT ON błąd zgłoszony przez THROW w otwartej transakcji wycofuje ją. Zgłaszanie błędu po ROLLBACK opisuje artykuł o TRY CATCH i THROW w procedurze zapisu.

IF @kolumny IS NULL THROW 50100, N'Nie odczytano kolumn tabeli.', 1;
IF NOT EXISTS (SELECT 1 FROM dbo.KonfiguracjaRoli WHERE Id = @Wzorzec) THROW 50101, N'Brak wiersza wzorcowego.', 1;
IF EXISTS (SELECT 1 FROM dbo.KonfiguracjaRoli w JOIN dbo.KonfiguracjaRoli x ON x.NazwaRoli = @NowaRola AND x.KodPozycji = w.KodPozycji WHERE w.Id = @Wzorzec)
BEGIN PRINT N'Wiersz już istnieje, pomijam.'; RETURN; END;
SELECT @maxId = MAX(Id) FROM dbo.KonfiguracjaRoli;
SET @sql = N'INSERT ' + @tabela + N' (' + @kolumny + N') SELECT ' + @wartosci + N' FROM ' + @tabela + N' WHERE Id = @Wzorzec;';
BEGIN TRAN;
EXEC sp_executesql @sql, N'@Wzorzec int, @NowaRola varchar(20), @NowaEtykieta nvarchar(100)', @Wzorzec, @NowaRola, @NowaEtykieta;
IF @@ROWCOUNT <> 1 THROW 50102, N'Klonowanie nie dodało dokładnie jednego wiersza.', 1;
IF (SELECT COUNT(*) FROM dbo.KonfiguracjaRoli WHERE Id > @maxId) <> 1
   OR NOT EXISTS (SELECT 1 FROM dbo.KonfiguracjaRoli WHERE Id > @maxId AND NazwaRoli = @NowaRola AND Etykieta = @NowaEtykieta)
    THROW 50103, N'Podmiana wartości nie trafiła w nowy wiersz.', 1;
IF @Zatwierdz = 1 COMMIT; ELSE BEGIN ROLLBACK; PRINT N'Przebieg próbny: zmiany wycofane.'; END;

Przykład ilustracyjny. Skrypt, część 2: bezpieczniki THROW, a potem transakcja z decyzją COMMIT albo ROLLBACK.

Gdy istnieje już wiersz nowej roli o kodzie wzorca, skrypt wypisuje „Wiersz już istnieje, pomijam.” i kończy bez zmian. Kolejne uruchomienie po zatwierdzeniu, w przykładzie trzecie, nie zmieniło liczby wierszy roli MAG2: nadal 1. Uruchomienie z @Wzorzec = 99 kończy się błędem 50101 z komunikatem „Brak wiersza wzorcowego.”, a stan tabeli się nie zmienia.

Przebieg próbny kończy się ROLLBACK

Parametr @Zatwierdz = 0 kończy transakcję przez ROLLBACK i komunikat „Przebieg próbny: zmiany wycofane.”, a stan zostaje bez zmian: MAG1 ma 3 wiersze, a ROLA3 jeden. Parametr @Zatwierdz = 1 dodaje jeden wiersz roli MAG2 z etykietą „Przyjęcie (wariant B)”. Porządek i opis dodatkowy pochodzą ze wzorca, data utworzenia z wartości domyślnej, a wersja i klucz pozycji są nowe albo przeliczone.

SELECT NazwaRoli, COUNT(*) AS Wierszy FROM dbo.KonfiguracjaRoli GROUP BY NazwaRoli ORDER BY NazwaRoli;

Przykład ilustracyjny. Zapytanie kontrolne po każdym przebiegu.

Numer identity nie wraca po wycofaniu: wiersz wstawiony po wycofanej próbie dostał numer 6 zamiast 5. Luka w numeracji nie jest błędem, więc ciągłości numerów nie należy zakładać. Liczba wierszy na rolę przed próbą i po niej jest taka sama, a po zatwierdzeniu przybywa tylko MAG2 (1). Tę samą regułę egzekwuje warunek w THROW 50103.

Liczba wierszy na rolę w trzech momentach przebiegu skryptuMakieta: liczba wierszy na rolę w trzech momentach przebiegu skryptu, z nowym wierszem roli MAG2 o numerze 6.Wyniki zapytańkontrola po każdym przebieguZapytanie kontrolneSELECT NazwaRoli, COUNT(*) AS Wierszy FROM dbo.KonfiguracjaRoli GROUP BY NazwaRoli ORDER BY NazwaRoli;NazwaRoliPrzedPo próbiePo zatwierdzeniuMAG1333MAG2001ROLA3111Komunikaty skryptu@Zatwierdz = 0Przebieg próbny: zmiany wycofane.powtórne uruchomienie po zatwierdzeniuWiersz już istnieje, pomijam.Nowy wiersz po zatwierdzeniuIdNazwaRoliKodPozycjiEtykietaPorzadekKluczPozycji6MAG2PRZYJECIEPrzyjęcie (wariant B)10MAG2/PRZYJECIENumer 5 zużył wycofany przebieg próbny, więc nowy wiersz ma numer 6.Makieta poglądowa, dane przykładowe
Makieta poglądowa, dane przykładowe. Liczba wierszy na rolę w trzech momentach przebiegu oraz nowy wiersz o numerze 6.

Zmianę treści istniejących wierszy, z kontrolą skrótu, a nie dodanie nowego wiersza, opisuje artykuł o zmianie zapytań zapisanych w tabeli konfiguracji.

Sesja sqlcmd bez przełącznika -I przerywa skrypt błędem 1934

Narzędzie sqlcmd bez opcji -I uruchamia sesję z QUOTED_IDENTIFIER OFF. Wtedy zapytanie z metodą XML .value() kończy się błędem 1934 (SELECT failed), a zwykły INSERT do tabeli z indeksem filtrowanym też błędem 1934 (INSERT failed). Z opcją -I sesja ma ustawienie ON, jak w SSMS.

sqlcmd -S <serwer> -d <baza> -E -b -I -f 65001 -i plik.sql

Przykład ilustracyjny. Opcja -b przerywa po błędzie, -I włącza QUOTED_IDENTIFIER, a -f 65001 czyta plik jako UTF-8. Plik zapisujemy z BOM.

Bez -I także polecenie CREATE UNIQUE INDEX z danych próbnych kończy się błędem 1934, więc dane próbne zakładamy w sesji z -I. Drugi z tych przypadków, INSERT na tabeli z indeksem filtrowanym, opisuje szerzej wspomniany wyżej artykuł o indeksie filtrowanym, a procedurę zapisaną z QUOTED_IDENTIFIER OFF artykuł o procedurze z QUOTED_IDENTIFIER OFF.

NULL w poleceniu i obcięta zmienna nie zgłaszają błędu

Przy błędnej nazwie tabeli lista kolumn jest NULL, więc cały tekst polecenia też, a sp_executesql z NULL niczego nie wykonuje i nie zgłasza błędu. Dlatego pierwszy bezpiecznik sprawdza listę kolumn.

Druga pułapka dotyczy długości. Lista kolumn o 7804 znakach, zapisana do zmiennej nvarchar(4000), została po cichu obcięta do 4000, więc zmienne na listę i na polecenie muszą być nvarchar(max). Nie sprawdzaliśmy kolumn okresu tabel temporalnych ani kolumn szyfrowanych, więc nie twierdzimy, że lista z sys.columns obsługuje każdy rodzaj kolumny.

Pytania przed napisaniem własnego klonowania wiersza

Zanim przeniesiesz wzorzec do własnej bazy, odpowiedz sobie na cztery pytania.

  • Kolumny: które kolumny konfiguracji mają się zmienić w kopii, a które mają dostać wartość domyślną albo nową wartość nadaną przez silnik.
  • Ponowne uruchomienie: czy skrypt ma być uruchamiany więcej niż raz i co ma się wtedy stać z istniejącym wierszem.
  • Przebieg próbny: kto i na jakiej bazie wykonuje przebieg próbny przed zatwierdzeniem.
  • Kontrola: jak sprawdzisz po zatwierdzeniu, że konfiguracja innych ról nie została zmieniona.

Bezpieczne składanie nazw przez QUOTENAME w innym zastosowaniu opisuje artykuł o podstawianiu pól w szablonie wiadomości. Wszystkie artykuły sekcji zebraliśmy w spisie artykułów o SQL Server.

FAQ

Najczęstsze pytania o klonowanie wiersza w T-SQL

01

Dlaczego listę kolumn pobiera się z sys.columns, a nie wpisuje ręcznie?

Lista z katalogu nadąża za schematem: dodanie kolumny do tabeli nie wymaga zmiany skryptu. Z jednego zapytania powstają dwie listy, kolumny docelowe i wartości, a w wartościach nazwa roli i etykieta są zastąpione parametrami. Każda nazwa przechodzi przez QUOTENAME, a kolejność wyznacza column_id.

02

Które kolumny trzeba pominąć przy INSERT SELECT z kopii wiersza?

Pomijamy cztery rodzaje kolumn: identity, obliczane, rowversion oraz Utworzono z wartością domyślną. Pozostałe, także Aktywna, kopiujemy z wiersza wzorcowego, a nazwa roli i etykieta pochodzą z parametrów.

03

Jak sprawić, żeby skrypt dało się uruchomić drugi raz bez skutków?

Skrypt sprawdza, czy istnieje już wiersz nowej roli o kodzie wzorca. Jeśli tak, wypisuje „Wiersz już istnieje, pomijam.” i kończy bez zmian. W przykładzie trzecie uruchomienie nie zmieniło liczby wierszy roli MAG2.

04

Co robi przebieg próbny i dlaczego numer identity bywa wtedy pominięty?

Przy @Zatwierdz = 0 skrypt kończy transakcję przez ROLLBACK i wypisuje „Przebieg próbny: zmiany wycofane.”, więc stan tabeli zostaje bez zmian. Numer identity nie wraca po wycofaniu: wiersz wstawiony po próbie dostał numer 6 zamiast 5. Luka w numeracji nie jest błędem.

05

Dlaczego ten sam skrypt w sqlcmd bez opcji -I kończy się błędem 1934?

Sqlcmd bez -I otwiera sesję z QUOTED_IDENTIFIER OFF. Wtedy zapytanie z metodą XML .value() i zwykły INSERT do tabeli z indeksem filtrowanym kończą się błędem 1934. Z opcją -I sesja ma ustawienie ON, jak w SSMS.

Słownik pojęć

Słownik pojęć

Pojęcia użyte we wzorcu klonowania wiersza konfiguracji.

Ssys.columns
Widok katalogu z jednym wierszem na kolumnę obiektu, z flagami is_identity i is_computed. We wzorcu składamy z niego listę kolumn do INSERT, w kolejności column_id.
Rrowversion
Typ danych, w którym silnik sam nadaje nową wartość przy każdej zmianie wiersza. Nie wolno do niego wstawiać wartości, więc przy klonowaniu kolumna jest pomijana, a kopia dostaje nową wersję.
QQUOTENAME
Funkcja otaczająca nazwę nawiasami kwadratowymi i podwajająca zamykający nawias. QUOTENAME(N'Opis dodatkowy') daje [Opis dodatkowy], a QUOTENAME(N'a]b') daje [a]]b].
Ssp_executesql
Procedura wykonująca tekst polecenia z parametrami. We wzorcu nazwę tabeli składamy z katalogu, a wartości przekazujemy parametrami, nie sklejając ich w tekst.
PPrzebieg próbny
Uruchomienie skryptu, które kończy transakcję przez ROLLBACK. We wzorcu parametr @Zatwierdz = 0 pokazuje komunikat o wycofaniu i zostawia stan bez zmian, choć numer identity jest zużyty.
IIndeks filtrowany
Indeks obejmujący wiersze spełniające warunek. W przykładzie unikatowy indeks pary nazwa roli i kod pozycji obejmuje tylko wiersze aktywne i wymaga QUOTED_IDENTIFIER ON przy INSERT.
SSkrypt idempotentny
Skrypt, który po ponownym uruchomieniu niczego nie zmienia. We wzorcu jest to komunikat „Wiersz już istnieje, pomijam.” zamiast drugiego wstawienia.

Zobacz to w praktyce

Sprawdź, jak systemy SoftwareStudio oparte na MS SQL Server wspierają ten obszar Twojej firmy.