Procedura przechowywana bazy danych z „trybem podglądu”

15

Dość powszechnym wzorcem w aplikacji bazy danych, z którą pracuję, jest potrzeba utworzenia procedury składowanej dla raportu lub narzędzia, które ma „tryb podglądu”. Gdy taka procedura dokonuje aktualizacji, ten parametr wskazuje, że wyniki akcji powinny zostać zwrócone, ale procedura nie powinna faktycznie wykonywać aktualizacji bazy danych.

Jednym ze sposobów na osiągnięcie tego jest po prostu napisanie ifinstrukcji dla parametru i posiadanie dwóch kompletnych bloków kodu; jeden z nich aktualizuje i zwraca dane, a drugi po prostu zwraca dane. Jest to jednak niepożądane ze względu na powielanie kodu i stosunkowo niski stopień pewności, że dane podglądu są w rzeczywistości dokładnym odzwierciedleniem tego, co stanie się z aktualizacją.

Poniższy przykład próbuje wykorzystać punkty zapisu transakcji i zmienne (na które transakcje nie mają wpływu, w przeciwieństwie do tabel tymczasowych), aby użyć tylko jednego bloku kodu dla trybu podglądu jako trybu aktualizacji na żywo.

Uwaga: Wycofywanie transakcji nie jest opcją, ponieważ to wywołanie procedury może być zagnieżdżone w transakcji. Jest to testowane na SQL Server 2012.

CREATE TABLE dbo.user_table (a int);
GO

CREATE PROCEDURE [dbo].[PREVIEW_EXAMPLE] (
  @preview char(1) = 'Y'
) AS

CREATE TABLE #dataset_to_return (a int);

BEGIN TRANSACTION; -- preview mode required infrastructure
  DECLARE @output_to_return TABLE (a int);
  SAVE TRANSACTION savepoint;

  -- do stuff here
  INSERT INTO dbo.user_table (a)
    OUTPUT inserted.a INTO @output_to_return (a)
    VALUES (42);

  -- catch preview mode
  IF @preview = 'Y'
    ROLLBACK TRANSACTION savepoint;

  -- save output to temp table if used for return data
  INSERT INTO #dataset_to_return (a)
  SELECT a FROM @output_to_return;
COMMIT TRANSACTION;

SELECT a AS proc_return_data FROM #dataset_to_return;
RETURN 0;
GO

-- Examples
EXEC dbo.PREVIEW_EXAMPLE @preview = 'Y';
SELECT a AS user_table_after_preview_mode FROM user_table;

EXEC dbo.PREVIEW_EXAMPLE @preview = 'N';
SELECT a AS user_table_after_live_mode FROM user_table;

-- Cleanup
DROP TABLE dbo.user_table;
DROP PROCEDURE dbo.PREVIEW_EXAMPLE;
GO

Szukam opinii na temat tego kodu i wzorca projektowego i / lub czy istnieją inne rozwiązania tego samego problemu w różnych formatach.

NReilingh
źródło

Odpowiedzi:

12

Istnieje kilka wad tego podejścia:

  1. Termin „podgląd” w większości przypadków może być dość mylący, w zależności od charakteru obsługiwanych danych (i to zmienia się z operacji na operację). Co ma zapewnić, że bieżące dane, na których operowane są dane, będą w tym samym stanie między momentem zebrania danych „podglądu” a powrotem użytkownika po 15 minutach - po zjedzeniu kawy, wyjściu na zewnątrz w celu palenia dymu, spacerze dookoła bloku, wracając i sprawdzając coś na eBayu - i zdaje sobie sprawę, że nie kliknęli przycisku „OK”, aby faktycznie wykonać operację, a więc w końcu klika przycisk?

    Czy masz limit czasu na kontynuowanie operacji po wygenerowaniu podglądu? A może sposób na określenie, czy dane są w tym samym stanie w czasie modyfikacji, co w SELECTmomencie początkowym ?

  2. Jest to drobna kwestia, ponieważ przykładowy kod mógł zostać wykonany w pośpiechu i nie reprezentowałby prawdziwego przypadku użycia, ale dlaczego miałby istnieć „podgląd” dla INSERToperacji? Może to mieć sens przy wstawianiu wielu wierszy za pomocą czegoś podobnego INSERT...SELECTi może być wstawiana zmienna liczba wierszy, ale nie ma to większego sensu dla operacji singleton.

  3. jest to niepożądane ze względu na ... stosunkowo niski stopień pewności, że dane podglądu są w rzeczywistości dokładnym odzwierciedleniem tego, co by się stało z aktualizacją.

    Skąd dokładnie bierze się ten „niski stopień zaufania”? Chociaż możliwe jest zaktualizowanie innej liczby wierszy niż wyświetlana, SELECTgdy wiele tabel jest DOŁĄCZONYCH, a zestaw wyników jest powielany, nie powinno to stanowić problemu. Wszelkie wiersze, na które powinien mieć wpływ, UPDATEmożna wybrać samodzielnie. Jeśli występuje niezgodność, zapytanie jest wykonywane nieprawidłowo.

    A sytuacje, w których występuje duplikacja z powodu tabeli JOINed, która pasuje do wielu wierszy w tabeli, która będzie aktualizowana, nie są sytuacjami, w których generowany byłby „Preview”. A jeśli istnieje taka sytuacja, należy wyjaśnić użytkownikowi, że jest aktualizowany podzbiór raportu, który jest powtarzany w raporcie, aby nie był to błąd, jeśli ktoś jest tylko patrząc na liczbę dotkniętych wierszy.

  4. Ze względu na kompletność (chociaż inne odpowiedzi o tym wspominały), nie używasz TRY...CATCHkonstrukcji, więc możesz łatwo napotkać problemy podczas zagnieżdżania tych wywołań (nawet jeśli nie używasz Save Points, a nawet nie używasz Transakcji). Proszę zobaczyć moją odpowiedź na następujące pytanie, tutaj na DBA.SE, dla szablonu, który obsługuje transakcje w zagnieżdżonych wywołaniach procedury składowanej:

    Czy jesteśmy zobowiązani do obsługi transakcji w kodzie C #, a także w procedurze przechowywanej

  5. NAWET JEŻELI wspomniane wyżej problemy zostały uwzględnione, nadal występuje poważna wada: przez krótki czas wykonywana jest operacja (tj. Przed ROLLBACK), wszelkie zapytania z błędnym odczytem (zapytania wykorzystujące WITH (NOLOCK)lub SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED) mogą przechwycić dane, które nie ma go chwilę później. Chociaż każdy, kto korzysta z brudnych odczytów, powinien już o tym wiedzieć i zaakceptować tę możliwość, takie operacje znacznie zwiększają szanse na wprowadzenie anomalii danych, które są bardzo trudne do debugowania (co oznacza: ile czasu chcesz poświęcić próbując znaleźć problem, który nie ma bezpośredniej bezpośredniej przyczyny?).

  6. Taki wzór obniża również wydajność systemu, zwiększając zarówno blokowanie, usuwając więcej blokad, jak i generując więcej aktywności w dzienniku transakcji. (Widzę teraz, że @MartinSmith wspomniał również o tych 2 kwestiach w komentarzu do pytania).

    Dodatkowo, jeśli modyfikowane są wyzwalacze w tabelach, może to być trochę dodatkowego przetwarzania (odczyty procesora i fizycznego / logicznego), które jest niepotrzebne. Wyzwalacze dodatkowo zwiększyłyby szanse na anomalie danych wynikające z brudnych odczytów.

  7. W związku z punktem wymienionym bezpośrednio powyżej - zwiększonymi blokadami - użycie Transakcji zwiększa prawdopodobieństwo wpadnięcia w impas, szczególnie jeśli zaangażowane są Wyzwalacze.

  8. Mniej poważny problem, który powinien odnosić się tylko do mniej prawdopodobnego scenariusza INSERToperacji: dane „Podglądu” mogą nie być takie same jak dane wstawiane w odniesieniu do wartości kolumn określonych przez DEFAULTograniczenia ( Sequences/ NEWID()/ NEWSEQUENTIALID()) i IDENTITY.

  9. Dodatkowy narzut związany z zapisywaniem zawartości zmiennej tabeli w tabeli tymczasowej nie jest potrzebny. ROLLBACKNie wpłynie na dane w zmiennej tabeli (co jest dlaczego powiedziałaś, że za pomocą zmiennych stole w pierwszej kolejności), więc byłoby bardziej sensowne, aby po prostu SELECT FROM @output_to_return;na końcu, a potem nawet nie przeszkadza tworzeniu tymczasowego Stół.

  10. Na wszelki wypadek ten niuans zapisywania punktów nie jest znany (trudny do odróżnienia z przykładowego kodu, ponieważ pokazuje tylko jedną procedurę składowaną): musisz użyć unikalnych nazw punktów zapisu, aby ROLLBACK {save_point_name}operacja zachowała się tak, jak tego oczekujesz. Jeśli użyjesz tych nazw ponownie, funkcja ROLLBACK przywróci ostatni punkt zapisu tej nazwy, który może nie znajdować się na tym samym poziomie zagnieżdżenia, z którego ROLLBACKjest wywoływany. Proszę zobaczyć pierwszy przykładowy blok kodu w następującej odpowiedzi, aby zobaczyć to zachowanie w akcji: Transakcja w procedurze przechowywanej

Sprowadza się to do:

  • Wykonanie „podglądu” nie ma większego sensu w operacjach zorientowanych na użytkownika. Robię to często dla operacji konserwacyjnych, aby zobaczyć, co zostanie usunięte / Garbage Collected, jeśli będę kontynuować operację. Dodaję opcjonalny parametr o nazwie @TestModei wykonuję IFinstrukcję, która albo robi, a SELECTkiedy @TestMode = 1robi to DELETE. Czasami dodaję @TestModeparametr do Procedur przechowywanych wywoływanych przez aplikację, dzięki czemu ja (i inni) mogę wykonywać proste testy bez wpływu na stan danych, ale aplikacja nigdy nie używa tego parametru.

  • Na wszelki wypadek nie było to jasne w górnej części „problemów”:

    Jeśli potrzebujesz / chcesz, aby tryb „Podgląd” / „Test” sprawdził, na co powinno wpłynąć wykonanie instrukcji DML, NIE używaj Transakcji (tj. BEGIN TRAN...ROLLBACKWzorca), aby to osiągnąć. Jest to wzorzec, który w najlepszym wypadku działa naprawdę tylko na systemie dla jednego użytkownika, a nawet nie jest to dobry pomysł w tej sytuacji.

  • Powtórzenie dużej części zapytania między dwiema gałęziami IFinstrukcji stanowi potencjalny problem z koniecznością aktualizacji obu z nich za każdym razem, gdy trzeba wprowadzić zmiany. Jednak różnice między tymi dwoma zapytaniami są zwykle dość łatwe do uchwycenia podczas przeglądu kodu i łatwe do naprawienia. Z drugiej strony problemy, takie jak różnice stanów i nieczytelne odczyty, są znacznie trudniejsze do znalezienia i rozwiązania. Problem zmniejszonej wydajności systemu jest niemożliwy do rozwiązania. Musimy rozpoznać i zaakceptować fakt, że SQL nie jest językiem zorientowanym obiektowo, a enkapsulacja / redukcja zduplikowanego kodu nie była celem projektowym SQL, tak jak w wielu innych językach.

    Jeśli zapytanie jest wystarczająco długie / złożone, możesz je zamknąć w funkcji Inline Valued Table. Następnie możesz zrobić proste SELECT * FROM dbo.MyTVF(params);dla trybu „Podgląd” i ŁĄCZYĆ się z kluczowymi wartościami dla trybu „zrób to”. Na przykład:

    UPDATE tab
    SET    tab.Col2 = tvf.ColB
           ...
    FROM   dbo.Table tab
    INNER JOIN dbo.MyTVF(params) tvf
            ON tvf.ColA = tab.Col1;
  • Jeśli jest to scenariusz raportu, o którym wspomniałeś, może to być uruchomienie raportu początkowego jako „Podgląd”. Jeśli ktoś chce zmienić coś, co widzi w raporcie (być może status), nie wymaga to dodatkowego podglądu, ponieważ oczekuje się zmiany aktualnie wyświetlanych danych.

    Jeśli operacja ma na celu zmianę kwoty oferty o określony% lub regułę biznesową, można to obsłużyć w warstwie prezentacji (JavaScript?).

  • Jeśli naprawdę potrzebujesz wykonać „Podgląd” dla operacji skierowanej do użytkownika końcowego , musisz najpierw uchwycić stan danych (być może skrót wszystkich pól w zestawie wyników dla UPDATEoperacji lub kluczowe wartości dla DELETEoperacji), a następnie, przed wykonaniem operacji, porównaj przechwycone informacje o stanie z bieżącymi informacjami - w ramach Transakcji wykonującej HOLDblokadę tabeli, aby nic się nie zmieniło po wykonaniu tego porównania - i jeśli istnieje jakakolwiek różnica, rzuć błąd i wykonaj ROLLBACKzamiast zamiast UPDATElub DELETE.

    W celu wykrycia różnic w UPDATEoperacjach alternatywą do obliczania skrótu w odpowiednich polach byłoby dodanie kolumny typu ROWVERSION . Wartość ROWVERSIONtypu danych zmienia się automatycznie przy każdej zmianie tego wiersza. Gdybyś miał taką kolumnę, zrobiłbyś SELECTto wraz z innymi danymi „Podglądu”, a następnie przekazałeś ją do kroku „pewnie, śmiało i zrób aktualizację” wraz z kluczowymi wartościami i wartościami zmienić. Następnie porównałbyś te ROWVERSIONwartości przekazane z „Podglądu” z bieżącymi wartościami (dla każdego klawisza) i kontynuowałbyś tylko UPDATEjeśli WSZYSTKOdopasowanych wartości. Zaletą jest to, że nie trzeba obliczać wartości skrótu, która może, nawet jeśli jest mało prawdopodobna, w przypadku fałszywie ujemnych wyników i zajmuje trochę czasu za każdym razem , gdy to zrobisz SELECT. Z drugiej strony ROWVERSIONwartość jest zwiększana automatycznie tylko po zmianie, więc nie musisz się o nic martwić. Jednak ROWVERSIONtyp to 8 bajtów, które mogą się sumować, gdy mamy do czynienia z wieloma tabelami i / lub wieloma wierszami.

    Każda z tych dwóch metod ma zalety i wady radzenia sobie z wykrywaniem niespójnego stanu związanego z UPDATEoperacjami, więc musisz określić, która metoda ma więcej „pro” niż „oszustwo” dla twojego systemu. Jednak w obu przypadkach można uniknąć opóźnienia między wygenerowaniem podglądu a wykonaniem operacji, powodując zachowanie wykraczające poza oczekiwania użytkownika końcowego.

  • Jeśli wykonujesz tryb „Podgląd” skierowany do użytkownika końcowego, oprócz rejestrowania stanu rekordów w czasie wyboru, przekazywania i sprawdzania w czasie modyfikacji, dołącz DATETIMEdo SelectTimei wypełnij za pomocą GETDATE()lub coś podobnego. Przekaż to do warstwy aplikacji, aby można ją było przekazać z powrotem do procedury składowanej (najprawdopodobniej jako pojedynczy parametr wejściowy), aby można ją było sprawdzić w procedurze przechowywanej. Następnie możesz ustalić, że JEŻELI operacja nie jest trybem „Podgląd”, wówczas @SelectTimewartość nie może przekraczać X minut przed bieżącą wartością GETDATE(). Może 2 minuty? 5 minut? Najprawdopodobniej nie więcej niż 10 minut. Zgłaszaj błąd, jeśli wartość DATEDIFFw MINUTES przekracza ten próg.

Solomon Rutzky
źródło
4

Najprostsze podejście jest często najlepsze i nie mam tak naprawdę problemu z duplikacją kodu w SQL, zwłaszcza nie w tym samym module. W końcu wszystkie dwa zapytania robią różne rzeczy. Dlaczego więc nie wybrać „Route 1” lub Keep It Simple i mieć po prostu dwie sekcje w przechowywanym proc, jeden do symulacji pracy, którą musisz wykonać, a drugi do wykonania, np. Coś takiego:

CREATE TABLE dbo.user_table ( rowId INT IDENTITY PRIMARY KEY, a INT NOT NULL, someGuid UNIQUEIDENTIFIER DEFAULT NEWID() );
GO
CREATE PROCEDURE [dbo].[PREVIEW_EXAMPLE2]

    @preview CHAR(1) = 'Y'

AS

    SET NOCOUNT ON

    --!!TODO add error handling

    IF @preview = 'Y'

        -- Simulate INSERT; could be more complex
        SELECT 
            ISNULL( ( SELECT MAX(rowId) FROM dbo.user_table ), 0 ) + 1 AS rowId,
            42 AS a,
            NEWID() AS someGuid

    ELSE

        -- Actually do the INSERT, return inserted values
        INSERT INTO dbo.user_table ( a )
        OUTPUT inserted.rowId, inserted.a, inserted.someGuid
        VALUES ( 42 )

    RETURN

GO

Ma to tę zaletę, że jest samo-dokumentujące (tzn. IF ... ELSEJest łatwe do naśladowania), ma niską złożoność (w porównaniu do punktu zapisu ze zmiennym podejściem do tabeli IMO), w związku z czym rzadziej ma błędy (świetne miejsce z @Cody).

Jeśli chodzi o twoje zdanie na temat niskiej pewności siebie, nie jestem pewien, czy rozumiem. Logicznie dwa zapytania o tych samych kryteriach powinny zrobić to samo. Istnieje możliwość niedopasowania liczności między UPDATEa a SELECT, ale byłaby cechą twoich złączeń i kryteriów. Czy możesz wyjaśnić dalej?

Na marginesie, powinieneś ustawić właściwość NULL/ NOT NULLoraz swoje tabele i zmienne tabel, rozważ ustawienie klucza podstawowego.

Twoje oryginalne podejście wydaje się nieco skomplikowane, może być bardziej podatne na zakleszczenia, ponieważ operacje INSERT/ UPDATE/ DELETEwymagają wyższych poziomów blokowania niż zwykłe SELECTs.

Podejrzewam, że twoje procesy w prawdziwym świecie są bardziej skomplikowane, więc jeśli uważasz, że powyższe podejście nie zadziała dla nich, odeślij więcej przykładów.

wBob
źródło
3

Moje obawy są następujące.

  • Obsługa transakcji nie przebiega zgodnie ze standardowym wzorcem zagnieżdżenia w bloku Begin Try / Begin Catch. Jeśli jest to szablon, wówczas w procedurze składowanej z kilkoma dodatkowymi krokami można wyjść z tej transakcji w trybie podglądu, gdy dane są nadal modyfikowane.

  • Przestrzeganie formatu zwiększa pracę programistów. Jeśli zmienią kolumny wewnętrzne, muszą również zmodyfikować definicję zmiennej tabeli, następnie zmodyfikować definicję tabeli temp, a następnie zmodyfikować wstawianie kolumn na końcu. To nie będzie popularne.

  • Niektóre procedury składowane nie zwracają za każdym razem tego samego formatu danych; pomyśl o sp_WhoIsActive jako wspólny przykład.

Nie podałem lepszego sposobu, aby to zrobić, ale nie sądzę, że to, co masz, to dobry wzór.

Cody Konior
źródło