SQL Server w logistyce

Szablon wiadomości z polami z tabeli i bezpieczne podstawianie w T-SQL

Nazwy tabeli i kolumny nie przechodzą parametrem, więc szablon wiadomości wymaga listy dozwolonych pól i QUOTENAME. Klucz trafia do sp_executesql jako parametr, a wynik sprawdza dziewięć przypadków.

sql.server.net.pl/sql-server/
Dłonie trzymają smartfon z tekstem wiadomości, obok unoszą się kartki papieru
Dłonie trzymają smartfon z tekstem wiadomości, obok unoszą się kartki papieru
W skrócie
  • Problem: nazwy tabeli i kolumny nie przechodzą do sp_executesql parametrem, bo są częścią tekstu zapytania.
  • Ochrona: lista dozwolonych pól sprawdzana przed zapytaniem oraz QUOTENAME przy budowie tekstu.
  • Wartość klucza: trafia do zapytania parametrem @id, a odczytana wartość wraca parametrem wyjściowym.
  • Test: dziewięć przypadków, w tym pole spoza listy z błędem 50012 i błędny token z błędem 50011.

Podstawianie pól w szablonie wiadomości T-SQL pozwala redaktorowi wskazać, które dane z bazy trafią do e-maila lub SMS-a, bez oddawania mu dostępu do całej bazy. Szablon zapisany w tabeli zawiera tokeny, a procedura zamienia je na wartości. Każda nazwa obiektu z tokenu trafia jednak do tekstu zapytania, więc wymaga osobnej ochrony.

Procedura rozbiera token na trzy części (tabela, kolumna, klucz), sprawdza je na liście dozwolonych pól i przez COL_LENGTH upewnia się, że kolumny istnieją. Nazwy otacza QUOTENAME przy budowie zapytania, a wartość klucza przekazuje parametrem do sp_executesql. Odczytana wartość wraca parametrem wyjściowym i nie trafia do tekstu zapytania.

Po co szablon wiadomości z polami w tabeli i gdzie jest w nim ryzyko

W przykładzie token ma postać [@POLE:tabela;kolumna;klucz], na przykład [@POLE:zgloszenia;numer_rma;id]. Wartość klucza podaje procedura jako parametr @id typu bigint, więc w przykładzie klucz jest liczbą. To wzorzec autorski, napisany od nowa z nazwami przykładowymi.

Dla porównania, w produkcie StudioSystem szablony e-mail i SMS mają własny token [@FIELD:tab#tabela;kol#kolumna;id#klucz], który składa treść wiadomości w momencie przygotowania do wysyłki. Pętla przetwarza najwyżej 99 tokenów, a brak wiersza daje w treści spację. Wzorzec z tego artykułu jest uproszczoną wersją z własną listą dozwolonych pól. Wiadomość wysyła pracownik, a procedura tylko składa jej treść. Od strony użytkownika opisuje to strona o wiadomości do klienta z szablonu, a to, co administrator zmienia sam, wyjaśnia strona o module reklamacji.

Ryzyko wynika z jednej cechy języka. Wartość klucza można przekazać do sp_executesql jako parametr, ale nazwy tabeli i kolumny nie, bo nie są wartościami, tylko częścią tekstu zapytania. Skoro redaktor szablonu wpisuje te nazwy w tokenie, procedura musi je wstawić do tekstu, a to wymaga dwóch zabezpieczeń naraz.

Pierwszym jest lista dozwolonych pól sprawdzana przed zapytaniem. Drugim jest QUOTENAME przy budowie tekstu. Samo QUOTENAME nie ogranicza dostępu, a nazwy doklejone bez niego łamią składnię, co pokazują dwie następne sekcje.

Token i jego rozbiór w T-SQL

Na początku tworzymy dane przykładowe i listę dozwolonych pól. Tabela zgloszenia ma kolumnę uwagi_wewnetrzne, której redaktor nie powinien mieć szansy wstawić do wiadomości. Drugie zgłoszenie ma w polu klienta tekst przypominający token, bo pozwoli to sprawdzić, że dane nie są rozwijane.

CREATE TABLE dbo.zgloszenia (
    id bigint NOT NULL PRIMARY KEY, klient nvarchar(100) NOT NULL, numer_rma varchar(20) NOT NULL,
    data_zgloszenia datetime NOT NULL, status nvarchar(30) NULL, uwagi_wewnetrzne nvarchar(200) NULL);
INSERT dbo.zgloszenia VALUES
 (1, N'Klient Testowy', '7C3A-1E94-B20F', '2026-10-12T14:30:00', N'Przyjęta', N'tylko dla serwisu'),
 (2, N'Klient [@POLE:abc]', '5D2B-8F01-C46A', '2026-10-13T09:00:00', NULL, N'tylko dla serwisu');

CREATE TABLE dbo.szablon_pola (
    tabela sysname NOT NULL, kolumna sysname NOT NULL, klucz sysname NOT NULL,
    PRIMARY KEY (tabela, kolumna));
INSERT dbo.szablon_pola VALUES
 (N'zgloszenia', N'klient', N'id'), (N'zgloszenia', N'numer_rma', N'id'),
 (N'zgloszenia', N'data_zgloszenia', N'id'), (N'zgloszenia', N'status', N'id');

Przykład ilustracyjny, dane wymyślone. Dane przykładowe i lista dozwolonych pól z kluczem głównym tabela plus kolumna.

Procedura składa się z jednej pętli. Pierwszy fragment znajduje token funkcją CHARINDEX i wycina jego treść między [@POLE: a ]. Potem dzieli ją po średnikach na trzy części. Jeśli brakuje zamknięcia nawiasu albo token nie ma dokładnie trzech części, procedura zgłasza THROW z numerem 50011 i komunikatem, że token ma mieć postać [@POLE:tabela;kolumna;klucz]. Wywołujący dostaje więc wyjątek, a nie pustą wiadomość. O tym, jak zachowuje się THROW po ROLLBACK w procedurze zapisu, pisze osobny artykuł.

Token z dwiema częściami, na przykład [@POLE:zgloszenia;klient], i token bez zamykającego ] kończą się w teście błędem 50011. Procedura powstaje przez CREATE OR ALTER, a THROW jest dostępny od SQL Server 2012.

CREATE OR ALTER PROCEDURE dbo.wypelnij_szablon
    @szablon nvarchar(max), @id bigint, @wynik nvarchar(max) OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @poz int = 1, @p int, @k int, @s1 int, @s2 int, @tok nvarchar(max),
            @tab nvarchar(128), @kol nvarchar(128), @klucz nvarchar(128), @sql nvarchar(max), @w nvarchar(4000);
    WHILE CHARINDEX(N'[@POLE:', @szablon, @poz) > 0
    BEGIN
        SET @p = CHARINDEX(N'[@POLE:', @szablon, @poz);
        SET @k = CHARINDEX(N']', @szablon, @p);
        IF @k = 0 THROW 50011, N'Token ma mieć postać [@POLE:tabela;kolumna;klucz].', 1;
        SET @tok = SUBSTRING(@szablon, @p + 7, @k - @p - 7);
        SET @s1 = CHARINDEX(N';', @tok);
        SET @s2 = CHARINDEX(N';', @tok, @s1 + 1);
        IF @s1 = 0 OR @s2 = 0 OR CHARINDEX(N';', @tok, @s2 + 1) > 0
            THROW 50011, N'Token ma mieć postać [@POLE:tabela;kolumna;klucz].', 1;
        SELECT @tab = LEFT(@tok, @s1 - 1), @kol = SUBSTRING(@tok, @s1 + 1, @s2 - @s1 - 1),
               @klucz = SUBSTRING(@tok, @s2 + 1, 4000);
        IF NOT EXISTS (SELECT 1 FROM dbo.szablon_pola
                       WHERE tabela = @tab AND kolumna = @kol AND klucz = @klucz
                         AND COL_LENGTH(N'dbo.' + QUOTENAME(tabela), kolumna) IS NOT NULL
                         AND COL_LENGTH(N'dbo.' + QUOTENAME(tabela), klucz) IS NOT NULL)
            THROW 50012, N'Pole spoza listy dozwolonych.', 1;

Przykład ilustracyjny, dane wymyślone. Pierwsza część procedury: rozbiór tokenu i sprawdzenie listy dozwolonych pól.

Schemat procedury podstawiania pól w szablonie wiadomości Schemat procedury podstawiania pól: rozbiór tokenu, sprawdzenie listy dozwolonych pól, zapytanie z parametrem, zamiana w tekście. Błędy tokenu i pola spoza listy kończą się wyjątkiem, a pętla przechodzi do następnego tokenu. Podstawianie pól w szablonie: lista dozwolonych pól przed zapytaniem Nazwy obiektów sprawdza lista i QUOTENAME, a wartość klucza trafia do zapytania parametrem. Szablon z tokenem [@POLE:tabela;kolumna;klucz] Rozbiór na trzy części (średnik) Trzy części i zamknięcie ]? tak Wpis (tabela, kolumna, klucz) na liście i kolumny istnieją? tak nie THROW 50011 nie THROW 50012 szablon_pola tabela kolumna klucz zgloszenia numer_rma id Zapytanie z QUOTENAME(kolumna) i QUOTENAME(tabela) sp_executesql z parametrem @id i wyjściem @w ISNULL(@w, pusty tekst) STUFF: zamiana tokenu na wartość; pozycja szukania za wstawioną wartością następny token
Schemat procedury: najpierw rozbiór i sprawdzenie tokenu na liście dozwolonych pól, potem zapytanie z parametrem klucza i zamiana tokenu na wartość.

Lista dozwolonych pól zamiast zaufania do tekstu szablonu

Sprawdzenie listy ma dwie części. Procedura szuka w tabeli szablon_pola wiersza zgodnego z trzema częściami tokenu, a przez COL_LENGTH upewnia się, że obie kolumny nadal istnieją. Klucz główny tej tabeli to tabela plus kolumna, więc dla jednej kolumny istnieje jeden klucz odczytu.

QUOTENAME chroni składnię, ale nie ogranicza dostępu: każda istniejąca kolumna dałaby się odczytać z szablonu. Dlatego lista jest osobnym zabezpieczeniem. W teście kolumna uwagi_wewnetrzne istnieje w tabeli zgloszenia, ale nie ma jej na liście. Token [@POLE:zgloszenia;uwagi_wewnetrzne;id] kończy się błędem 50012 z komunikatem, że pole jest spoza listy dozwolonych. To samo dzieje się dla nazwy kolumny z apostrofem i dla tokenu wskazującego widok systemowy.

Dopisanie wiersza do szablon_pola jest decyzją osoby odpowiedzialnej za dane, a nie edycją szablonu. Filtrowanie wierszy według tożsamości sesji to osobny wzorzec z artykułu o funkcji tożsamości w SQL Server.

Lista starzeje się po zmianie schematu. Po ALTER TABLE zgloszenia DROP COLUMN status zapytanie kontrolne zwraca jeden wiersz (zgloszenia, status, id), czyli wpis, który wskazuje usuniętą kolumnę.

SELECT p.tabela, p.kolumna, p.klucz
FROM dbo.szablon_pola AS p
WHERE COL_LENGTH(N'dbo.' + QUOTENAME(p.tabela), p.kolumna) IS NULL
   OR COL_LENGTH(N'dbo.' + QUOTENAME(p.tabela), p.klucz) IS NULL;

Przykład ilustracyjny. Zapytanie kontrolne pokazuje wpisy listy wskazujące kolumny, których już nie ma.

Zapytanie z QUOTENAME i parametrem klucza

Druga część procedury buduje tekst zapytania i wykonuje je. Wszystkie trzy nazwy z tokenu przechodzą przez QUOTENAME, który otacza nazwę nawiasami kwadratowymi i podwaja znak ]. Wartość klucza idzie parametrem @id, a odczytana wartość wraca parametrem wyjściowym @w typu nvarchar(4000).

        SET @sql = N'SELECT @w = CONVERT(nvarchar(4000), ' + QUOTENAME(@kol) + N', 23) FROM dbo.'
                 + QUOTENAME(@tab) + N' WHERE ' + QUOTENAME(@klucz) + N' = @id;';
        SET @w = NULL;
        EXEC sys.sp_executesql @sql, N'@id bigint, @w nvarchar(4000) OUTPUT', @id = @id, @w = @w OUTPUT;
        SET @w = ISNULL(@w, N'');
        SET @szablon = STUFF(@szablon, @p, @k - @p + 1, @w);
        SET @poz = @p + LEN(@w + N'.') - 1;
    END
    SET @wynik = @szablon;
END

Przykład ilustracyjny, dane wymyślone. Ciąg dalszy tej samej procedury: zapytanie z QUOTENAME, sp_executesql i zamiana tokenu w tekście.

Obie części tworzą jedną procedurę, a druga zaczyna się wewnątrz tej samej pętli. Nazwy z listy są już sprawdzone, więc QUOTENAME chroni tylko składnię.

Parametrem można przekazać wartość klucza, ale nie nazwę kolumny, więc nazwy trzeba najpierw sprawdzić na liście, a dopiero potem otoczyć QUOTENAME.

Co daje samo QUOTENAME, pokazuje test z nazwą kolumny numer'rma. Doklejona do zapytania bez QUOTENAME daje błąd składni, Msg 105: Unclosed quotation mark after the character string 'rma FROM dbo.zgloszenia'. Z QUOTENAME zapytanie jest poprawne składniowo i kończy się Msg 207: Invalid column name 'numer'rma'. Dowodem jest więc wyjątek składni albo nazwy, a nie zmieniony wynik.

Data w kolumnie datetime jest zamieniana przez CONVERT ze stylem 23 i daje postać 2026-10-12, czyli samą datę w formacie ISO. Bez stylu wynik dla datetime zależałby od ustawień sesji, dlatego przykład podaje go jawnie.

Wartość NULL w kolumnie daje pusty tekst: w teście pusty status zostawia w wiadomości zdanie Status: .. Brak wiersza o podanym kluczu, na przykład dla id = 999, też daje pusty tekst i zdanie Numer: .. Procedura nie rzuca w tych dwóch przypadkach błędu. To decyzja wzorca, którą łatwo zmienić na THROW, jeśli pusta wartość ma zatrzymywać wysyłkę.

Test na dziewięciu przypadkach

Test obejmuje dziewięć przypadków. Pierwszy to komplet pól dla zgłoszenia 1, a kolejne osiem sytuacji zestawia tabela poniżej. Wywołanie pojedynczego przypadku wygląda tak jak poniżej.

DECLARE @w nvarchar(max);
EXEC dbo.wypelnij_szablon
     N'Zgłoszenie [@POLE:zgloszenia;numer_rma;id] ma status: [@POLE:zgloszenia;status;id].', 1, @w OUTPUT;
SELECT @w AS wiadomosc;

Przykład ilustracyjny, dane wymyślone. Wywołanie skróconego szablonu o dwóch tokenach dla zgłoszenia 1 zwraca Zgłoszenie 7C3A-1E94-B20F ma status: Przyjęta. Pełny szablon z czterema tokenami sprawdza przypadek 1.

Dziewięć przypadków testu procedury podstawiania pól
NrPrzypadekWynik
1komplet pól, id 1Dzień dobry, Klient Testowy. Zgłoszenie 7C3A-1E94-B20F z dnia 2026-10-12 ma status: Przyjęta.
2token w danych klienta, id 2Klient: Klient [@POLE:abc]
3NULL w kolumnie, id 2Status: .
4brak wiersza, id 999Numer: .
5kolumna spoza listyBŁĄD 50012: Pole spoza listy dozwolonych.
6apostrof w nazwie kolumnyBŁĄD 50012: Pole spoza listy dozwolonych.
7widok systemowy w tokenieBŁĄD 50012: Pole spoza listy dozwolonych.
8dwie części tokenuBŁĄD 50011: Token ma mieć postać [@POLE:tabela;kolumna;klucz].
9brak zamknięcia ]BŁĄD 50011: Token ma mieć postać [@POLE:tabela;kolumna;klucz].

Wyniki testu na danych przykładowych. Dziewięć przypadków, w tym pięć błędów.

Wynik testu procedury podstawiania pól: dziewięć przypadków Makieta poglądowa, dane przykładowe. Wynik testu procedury w siatce: dziewięć przypadków z wynikiem lub numerem błędu. Wyniki 9 przypadków testu Przykład wywołania DECLARE @w nvarchar(max); EXEC dbo.wypelnij_szablon N'Zgłoszenie [@POLE:zgloszenia;numer_rma;id] ma status: [@POLE:zgloszenia;status;id].', 1, @w OUTPUT; SELECT @w AS wiadomosc; Wyniki nr przypadek rezultat 1 komplet pól, id 1 Dzień dobry, Klient Testowy. Zgłoszenie 7C3A-1E94-B20F z dnia 2026-10-12 ma status: Przyjęta. 2 token w danych klienta, id 2 Klient: Klient [@POLE:abc] 3 NULL w kolumnie, id 2 Status: . 4 brak wiersza, id 999 Numer: . 5 kolumna spoza listy BŁĄD 50012: Pole spoza listy dozwolonych. 6 apostrof w nazwie kolumny BŁĄD 50012: Pole spoza listy dozwolonych. 7 widok systemowy w tokenie BŁĄD 50012: Pole spoza listy dozwolonych. 8 dwie części tokenu BŁĄD 50011: Token ma mieć postać [@POLE:tabela;kolumna;klucz]. 9 brak zamknięcia ] BŁĄD 50011: Token ma mieć postać [@POLE:tabela;kolumna;klucz]. 9 wierszy Makieta poglądowa, dane przykładowe
Makieta poglądowa, dane przykładowe: wynik testu procedury w siatce, z błędami w przypadkach od 5 do 9.

Przypadek 2 pokazuje, że tekst przypominający token w danych nie jest rozwijany: wynik to Klient: Klient [@POLE:abc]. Zapewnia to przesuwanie pozycji szukania za wstawioną wartość, czyli instrukcja @poz = @p + długość wartości. Pętla, która po każdej zamianie szukałaby tokenu od początku tekstu, rozwijałaby także tokeny znajdujące się we wstawionych wartościach, więc sama lista dozwolonych pól by nie wystarczyła.

Do długości wstawionej wartości procedura dodaje kropkę: LEN(@w + N'.') - 1. Funkcja LEN ignoruje końcowe spacje, więc bez tego wartość z końcową spacją przesunęłaby pozycję o jeden znak za mało.

Granice wzorca

Każdy token to osobne wywołanie sp_executesql. Wzorzec nie grupuje odczytów z jednego wiersza, więc szablon z wieloma tokenami dla jednego zgłoszenia wykonuje tyle zapytań, ile ma tokenów. Wartość dłuższa niż 4000 znaków zostałaby obcięta przez nvarchar(4000) parametru wyjściowego.

Przykład uruchomiliśmy na silniku SQL Server 2022 w jednorazowej bazie z poziomem zgodności 130, a nie na silniku 2016. Kod nie używa funkcji nowszych niż SQL Server 2016 SP1. Lista dozwolonych pól starzeje się po zmianie schematu, dlatego zapytanie kontrolne ma swoje miejsce w utrzymaniu bazy.

Przykład: administrator dopisuje do szablonu potwierdzenia token [@POLE:zgloszenia;numer_rma;id] i zapisuje szablon w tabeli. Gdy pracownik wysyła wiadomość dla zgłoszenia 1 (Klient Testowy, numer RMA 7C3A-1E94-B20F), procedura odczytuje numer parametrem @id i wstawia go do tekstu. Następnego dnia administrator, próbując dodać do szablonu [@POLE:zgloszenia;uwagi_wewnetrzne;id], dostaje błąd 50012, bo kolumny nie ma na liście dozwolonych pól.

Szczegóły wysyłki opisuje strona o wysyłaniu e-maili z programu. Walidację parametrów pokazuje artykuł o API na procedurach SQL i JSON, a sp_executesql w innym zastosowaniu artykuł o QUOTED_IDENTIFIER OFF w procedurze. Zmianę treści zapisanych w tabeli konfiguracji opisuje artykuł o zmianie zapytań z kontrolą skrótu.

Cztery pytania przed budową własnych szablonów z polami

Zanim zbudujesz podobny mechanizm u siebie, odpowiedz na cztery pytania.

  • Redaktor: kto w organizacji redaguje szablony wiadomości i czy może wskazywać dowolne kolumny, czy tylko zatwierdzoną listę.
  • Zakaz: które kolumny nie mogą trafić do wiadomości do klienta pod żadnym warunkiem.
  • Puste wartości: czy brak wiersza lub pusta wartość mają dać pusty tekst, czy zatrzymać wysyłkę błędem.
  • Klucz odczytu: czy jest liczbą, czy tekstem, i czy jedna tabela wystarczy do wszystkich pól szablonu.
FAQ

Najczęstsze pytania o podstawianie pól w szablonie wiadomości

01

Dlaczego nazwy tabeli i kolumny nie mogą być parametrami sp_executesql?

Parametrem można przekazać wartość klucza, ale nazwy tabeli i kolumny nie są wartościami, tylko częścią tekstu zapytania. Dlatego wartość odczytana z kolumny wraca parametrem wyjściowym, a nazwy trzeba wstawić do tekstu po sprawdzeniu.

02

Czy samo QUOTENAME wystarcza do bezpiecznego podstawiania?

Nie. QUOTENAME otacza nazwę nawiasami i podwaja znak ], więc chroni składnię: nazwa z apostrofem kończy się błędem nazwy zamiast błędu składni. Nie ogranicza jednak dostępu, bo każda istniejąca kolumna dałaby się odczytać, dlatego procedura sprawdza też listę dozwolonych pól i zgłasza błąd 50012 dla pola spoza listy.

03

Co się stanie, gdy w danych klienta znajdzie się tekst podobny do tokenu?

Trafi do wiadomości w niezmienionej postaci: w teście pole klienta z tekstem Klient [@POLE:abc] dało wynik Klient: Klient [@POLE:abc]. Zapewnia to przesuwanie pozycji szukania za wstawioną wartość, bo pętla szukająca od początku tekstu rozwijałaby także takie tokeny.

04

Jak procedura traktuje brak wiersza i wartość NULL?

Oba przypadki dają pusty tekst. Dla pustego statusu wiadomość zawiera Status: ., a dla id równego 999 Numer: ., bez błędu. To decyzja wzorca, którą łatwo zmienić na THROW.

05

Jak wykryć wpisy listy dozwolonych pól, które wskazują usuniętą kolumnę?

Zapytaniem kontrolnym, które sprawdza COL_LENGTH dla kolumny i klucza każdego wpisu i zwraca wpisy z wynikiem NULL. Po usunięciu kolumny status z tabeli zgloszenia zwróciło jeden wiersz: zgloszenia, status, id.

Słownik pojęć

Słownik pojęć

Pojęcia z kodu i przykładu opisanych w artykule.

TToken szablonu
Znacznik w tekście wiadomości, który procedura zamienia na wartość z bazy. W przykładzie ma postać [@POLE:tabela;kolumna;klucz], a składnię i zamknięcie nawiasu sprawdza błąd 50011.
LLista dozwolonych pól
Tabela wpisów tabela, kolumna, klucz, które wolno wstawiać do wiadomości. Pole spoza listy kończy podstawianie błędem 50012, nawet jeśli kolumna istnieje.
QQUOTENAME
Funkcja otaczająca nazwę obiektu nawiasami kwadratowymi i podwajająca znak ]. Chroni składnię zapytania, ale nie ogranicza dostępu do kolumn.
PParametr wyjściowy
Parametr procedury albo sp_executesql, przez który wraca wartość. W przykładzie odczytana wartość wraca parametrem @w typu nvarchar(4000) i nie jest wklejana do tekstu zapytania.
Ssp_executesql
Procedura systemowa wykonująca zapytanie zbudowane jako tekst, z parametrami. Wartość klucza przekazuje parametrem, ale nazwy tabeli i kolumny są częścią tekstu.
RRozwinięcie wstawionej wartości
Błąd pętli, która po zamianie tokenu szuka następnego od początku tekstu i rozwija tokeny ze wstawionych wartości. Wzorzec unika go, przesuwając pozycję szukania za wstawioną wartość.
CCOL_LENGTH
Funkcja zwracająca długość kolumny albo NULL, gdy kolumny nie ma. Procedura używa jej do sprawdzenia, czy kolumny z listy dozwolonych pól nadal istnieją.
SStyl konwersji 23
Styl funkcji CONVERT zwracający datę w formacie ISO, na przykład 2026-10-12. Jawny styl sprawia, że wynik nie zależy od ustawień sesji.

Zobacz to w praktyce

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