Gespeicherte Datenbankprozedur mit einem "Vorschaumodus"

15

Ein recht verbreitetes Muster in der Datenbankanwendung, mit der ich arbeite, ist die Notwendigkeit, eine gespeicherte Prozedur für einen Bericht oder ein Dienstprogramm mit einem "Vorschaumodus" zu erstellen. Wenn eine solche Prozedur aktualisiert wird, gibt dieser Parameter an, dass die Ergebnisse der Aktion zurückgegeben werden sollen, die Aktualisierung der Datenbank jedoch nicht durchgeführt werden soll.

Eine Möglichkeit, dies zu erreichen, besteht darin, einfach eine ifAnweisung für den Parameter zu schreiben und zwei vollständige Codeblöcke zu erstellen. Einer aktualisiert und gibt Daten zurück und der andere gibt nur die Daten zurück. Dies ist jedoch unerwünscht, da der Code doppelt vorhanden ist und nur ein relativ geringes Maß an Sicherheit besteht, dass die Vorschaudaten tatsächlich genau wiedergeben, was bei einem Update passieren würde.

Im folgenden Beispiel wird versucht, Transaktionssicherungspunkte und -variablen (die im Gegensatz zu temporären Tabellen nicht von Transaktionen betroffen sind) zu nutzen, um nur einen einzigen Codeblock für den Vorschaumodus als Live-Aktualisierungsmodus zu verwenden.

Anmerkung: Transaktions- Rollbacks sind keine Option, da dieser Prozeduraufruf möglicherweise selbst in einer Transaktion verschachtelt ist. Dies wird unter SQL Server 2012 getestet.

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

Ich suche Feedback zu diesem Code und Entwurfsmuster und / oder zu anderen Lösungen für dasselbe Problem in verschiedenen Formaten.

NReilingh
quelle

Antworten:

12

Dieser Ansatz weist mehrere Mängel auf:

  1. Der Begriff "Vorschau" kann in den meisten Fällen sehr irreführend sein, abhängig von der Art der Daten, mit denen gearbeitet wird (und die sich von Vorgang zu Vorgang ändern). Was ist zu gewährleisten, dass sich die aktuellen Daten, die gerade bearbeitet werden, zwischen der Erfassung der "Vorschau" -Daten und der Rückkehr des Benutzers nach 15 Minuten in demselben Zustand befinden? Rund um den Block, wieder rein und etwas bei eBay nachsehen - und merkt man, dass sie nicht auf die Schaltfläche "OK" geklickt haben, um den Vorgang tatsächlich durchzuführen und klickt dann endlich auf die Schaltfläche?

    Haben Sie eine zeitliche Begrenzung für die Ausführung des Vorgangs, nachdem die Vorschau erstellt wurde? Oder möglicherweise eine Möglichkeit, um festzustellen, ob sich die Daten zum Zeitpunkt der Änderung im selben Status befinden wie zum ersten SELECTMal?

  2. Dies ist ein kleiner Punkt, da der Beispielcode möglicherweise hastig erstellt wurde und keinen echten Anwendungsfall darstellt. Warum sollte es jedoch eine "Vorschau" für eine INSERTOperation geben? Das kann sinnvoll sein, wenn Sie mehrere Zeilen über so etwas wie INSERT...SELECTeinfügen, und es kann eine variable Anzahl von Zeilen eingefügt werden, aber dies ist für eine Singleton-Operation nicht sehr sinnvoll.

  3. Dies ist unerwünscht, weil ... ein relativ geringer Grad an Sicherheit besteht, dass die Vorschaudaten tatsächlich genau wiedergeben, was bei einem Update passieren würde.

    Woher kommt genau dieses "geringe Vertrauen"? Es ist zwar möglich, eine andere Anzahl von Zeilen zu aktualisieren als für eine, SELECTwenn mehrere Tabellen verbunden sind und doppelte Zeilen in einer Ergebnismenge vorhanden sind, dies sollte hier jedoch kein Problem darstellen. Alle Zeilen, die von einem betroffen sein sollen, können einzeln ausgewählt UPDATEwerden. Wenn es eine Nichtübereinstimmung gibt, führen Sie die Abfrage falsch aus.

    In Situationen, in denen aufgrund einer JOINed-Tabelle, die mit mehreren Zeilen in der zu aktualisierenden Tabelle übereinstimmt, Duplikate auftreten, wird keine "Vorschau" generiert. Und wenn es eine Gelegenheit gibt, in der dies der Fall ist, muss dem Benutzer erklärt werden, dass er eine Teilmenge des Berichts aktualisiert, die im Bericht wiederholt wird, so dass es nicht als Fehler erscheint, wenn es sich nur um jemanden handelt Betrachten Sie die Anzahl der betroffenen Zeilen.

  4. Der Vollständigkeit halber (obwohl in den anderen Antworten dies erwähnt wurde), verwenden Sie das TRY...CATCHKonstrukt nicht. Daher können beim Verschachteln dieser Aufrufe leicht Probleme auftreten (auch wenn Sie keine Punkte speichern und keine Transaktionen verwenden). In meiner Antwort auf die folgende Frage hier auf DBA.SE finden Sie eine Vorlage, die Transaktionen über verschachtelte Aufrufe von gespeicherten Prozeduren hinweg verarbeitet:

    Müssen wir Transaktionen sowohl im C # -Code als auch in gespeicherten Prozeduren abwickeln?

  5. AUCH WENN die oben genannten Probleme berücksichtigt wurden, liegt immer noch ein kritischer Fehler vor: In der kurzen Zeit, in der der Vorgang ausgeführt wird (dh vor dem ROLLBACK), können alle Dirty-Read-Abfragen (Abfragen, die WITH (NOLOCK)oder verwenden SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED) Daten abrufen, die gibt es keinen Moment später. Während jeder, der Dirty-Read-Abfragen verwendet, sich dessen bewusst sein sollte und diese Möglichkeit akzeptiert hat, erhöhen Vorgänge wie diese die Wahrscheinlichkeit, dass Datenanomalien auftreten, die nur schwer zu debuggen sind (was bedeutet, wie viel Zeit Sie damit verbringen möchten, dies zu versuchen) Finden Sie ein Problem, das keine offensichtliche direkte Ursache hat?).

  6. Ein solches Muster verschlechtert auch die Systemleistung, indem es sowohl das Blockieren durch Aufheben weiterer Sperren erhöht als auch mehr Transaktionsprotokollaktivität generiert. (Ich sehe jetzt, dass @MartinSmith diese beiden Punkte auch in einem Kommentar zur Frage erwähnt hat.)

    Wenn außerdem Trigger für die zu ändernden Tabellen vorhanden sind, ist möglicherweise eine Menge zusätzlicher Verarbeitungsschritte (CPU- und physische / logische Lesevorgänge) nicht erforderlich. Trigger würden auch die Wahrscheinlichkeit von Datenanomalien, die sich aus unsauberen Lesevorgängen ergeben, weiter erhöhen.

  7. In Bezug auf den oben genannten Punkt - erhöhte Sperren - erhöht die Verwendung der Transaktion die Wahrscheinlichkeit, in Deadlocks zu geraten, insbesondere wenn Trigger involviert sind.

  8. Ein weniger schwerwiegendes Problem, das sich nur auf das weniger wahrscheinliche Szenario von INSERTVorgängen beziehen sollte : Die "Vorschau" -Daten stimmen möglicherweise nicht mit denen überein, die in Bezug auf die durch DEFAULTEinschränkungen ( Sequences/ NEWID()/ NEWSEQUENTIALID()) und festgelegten Spaltenwerte eingefügt wurden IDENTITY.

  9. Es ist kein zusätzlicher Aufwand erforderlich, um den Inhalt der Tabellenvariablen in die temporäre Tabelle zu schreiben. Dies ROLLBACKwürde sich nicht auf die Daten in der Tabellenvariablen auswirken (weshalb Sie angegeben haben, dass Sie zunächst Tabellenvariablen verwenden). Es wäre also sinnvoller, einfach SELECT FROM @output_to_return;am Ende zu arbeiten und sich dann nicht einmal die Mühe zu machen, die temporäre Variable zu erstellen Tabelle.

  10. Nur für den Fall, dass diese Nuance der Speicherpunkte nicht bekannt ist (im Beispielcode schwer zu erkennen, da nur eine einzelne gespeicherte Prozedur angezeigt wird): Sie müssen eindeutige Speicherpunktnamen verwenden, damit sich der ROLLBACK {save_point_name}Vorgang so verhält, wie Sie es erwarten. Wenn Sie die Namen wiederverwenden, wird durch einen ROLLBACK der letzte Speicherpunkt dieses Namens zurückgesetzt, der möglicherweise nicht auf derselben Verschachtelungsebene ROLLBACKliegt, von der aus der aufgerufen wird. Weitere Informationen zu diesem Verhalten finden Sie im ersten Beispielcodeblock in der folgenden Antwort: Transaktion in einer gespeicherten Prozedur

Worauf es ankommt, ist:

  • Das Ausführen einer "Vorschau" ist für Vorgänge mit Blick auf den Benutzer nicht sehr sinnvoll. Ich mache dies häufig für Wartungsvorgänge, damit ich sehen kann, was gelöscht / Garbage Collected wird, wenn ich mit dem Vorgang fortfahre. Ich füge einen optionalen Parameter namens hinzu @TestModeund mache eine IFAnweisung, die entweder a tut, SELECTwenn es @TestMode = 1sonst das tut DELETE. Manchmal füge ich den @TestModeParameter zu gespeicherten Prozeduren hinzu, die von der Anwendung aufgerufen werden, damit ich (und andere) einfache Tests durchführen können, ohne den Status der Daten zu beeinflussen, aber dieser Parameter wird von der Anwendung nie verwendet.

  • Nur für den Fall, dass dies nicht aus dem oberen Abschnitt der "Probleme" hervorgeht:

    Wenn Sie einen "Vorschau" - / "Test" -Modus benötigen / möchten, um zu sehen, was betroffen sein sollte, wenn die DML-Anweisung ausgeführt werden soll, verwenden Sie NICHT Transaktionen (dh das BEGIN TRAN...ROLLBACKMuster), um dies zu erreichen. Es ist ein Muster, das bestenfalls auf einem Einzelplatzsystem wirklich funktioniert und in dieser Situation nicht einmal eine gute Idee ist.

  • Wenn Sie den Großteil der Abfrage zwischen den beiden Zweigen der IFAnweisung wiederholen, besteht möglicherweise das Problem, dass beide bei jeder Änderung aktualisiert werden müssen. Unterschiede zwischen den beiden Abfragen sind jedoch in der Regel leicht zu erkennen und zu beheben. Andererseits sind Probleme wie Statusunterschiede und Dirty Reads viel schwieriger zu finden und zu beheben. Und das Problem der verringerten Systemleistung ist nicht zu beheben. Wir müssen erkennen und akzeptieren, dass SQL keine objektorientierte Sprache ist, und dass das Einkapseln / Reduzieren von dupliziertem Code kein Entwurfsziel von SQL war, wie es bei vielen anderen Sprachen der Fall war.

    Wenn die Abfrage lang / komplex genug ist, können Sie sie in eine Inline-Tabellenwertfunktion einbetten. Dann können Sie SELECT * FROM dbo.MyTVF(params);für den "Vorschau" -Modus eine einfache und für den "Do it" -Modus eine VERBINDUNG zu den Schlüsselwerten ausführen. Beispielsweise:

    UPDATE tab
    SET    tab.Col2 = tvf.ColB
           ...
    FROM   dbo.Table tab
    INNER JOIN dbo.MyTVF(params) tvf
            ON tvf.ColA = tab.Col1;
  • Wenn dies ein Berichtsszenario ist, wie Sie es bereits erwähnt haben, wird der erste Bericht in der "Vorschau" ausgeführt. Wenn jemand etwas ändern möchte, das er im Bericht sieht (z. B. einen Status), ist keine zusätzliche Vorschau erforderlich, da erwartet wird, dass die aktuell angezeigten Daten geändert werden.

    Wenn die Operation möglicherweise einen Gebotsbetrag um einen bestimmten Prozentsatz oder eine bestimmte Geschäftsregel ändern soll, kann dies in der Präsentationsebene (JavaScript?) Behandelt werden.

  • Wenn Sie wirklich eine "Vorschau" für eine Endbenutzeroperation durchführen müssen , müssen Sie zuerst den Status der Daten erfassen (möglicherweise ein Hash aller Felder in der Ergebnismenge für UPDATEOperationen oder die Schlüsselwerte für DELETEVergleichen Sie dann vor dem Ausführen der Operation die erfassten Statusinformationen mit den aktuellen Informationen - innerhalb einer Transaktion , die eine HOLDSperre für die Tabelle vornimmt, damit sich nach diesem Vergleich nichts ändert - und werfen Sie ein, wenn ein Unterschied vorliegt Fehler und mach ROLLBACKlieber ein als mit dem UPDATEoder fortzufahren DELETE.

    UPDATEEine Alternative zur Berechnung eines Hashs für die relevanten Felder besteht darin, eine Spalte vom Typ ROWVERSION hinzuzufügen, um Unterschiede für Operationen zu erkennen . Der Wert eines ROWVERSIONDatentyps ändert sich automatisch bei jeder Änderung dieser Zeile. Wenn Sie eine solche Spalte hätten, würden Sie SELECTsie zusammen mit den anderen "Vorschau" -Daten und dann zusammen mit dem (den) Schlüsselwert (en) und Wert (en) an den Schritt "Sicher, mach weiter und aktualisiere" weitergeben. wechseln. Sie würden dann die ROWVERSIONaus der "Vorschau" übergebenen Werte mit den aktuellen Werten (pro Taste) vergleichen und nur mit dem UPDATEif ALL fortfahrender Werte stimmen überein. Der Vorteil hierbei ist, dass Sie keinen Hash berechnen müssen, der das Potenzial hat, auch wenn es unwahrscheinlich ist, dass er falsch negativ ist, und jedes Mal etwas Zeit in Anspruch nimmt, wenn Sie den Hash ausführen SELECT. Andererseits wird der ROWVERSIONWert nur dann automatisch erhöht, wenn er geändert wird, sodass Sie sich keine Sorgen mehr machen müssen. Der ROWVERSIONTyp ist jedoch 8 Byte, was sich bei vielen Tabellen und / oder Zeilen summieren kann.

    Jede dieser beiden Methoden hat Vor- und Nachteile, UPDATEwenn es darum geht , inkonsistente Zustände im Zusammenhang mit Vorgängen zu erkennen. Sie müssen also ermitteln, welche Methode für Ihr System mehr Vorteile als Nachteile bietet. In beiden Fällen können Sie jedoch vermeiden, dass zwischen dem Erstellen der Vorschau und dem Ausführen des Vorgangs Verzögerungen auftreten, die zu einem Verhalten führen, das außerhalb der Erwartungen des Endbenutzers liegt.

  • Wenn Sie einen Endbenutzer-facing „Vorschau“ -Modus tun, dann zusätzlich zu dem Zustand der Aufzeichnungen in ausgewählter Zeit erfassen, die entlang und Kontrolle bei Änderung Zeit umfasst eine DATETIMEfür SelectTimeund bevölkert über GETDATE()oder etwas ähnliches. Übergeben Sie dies an die App-Ebene, damit es an die gespeicherte Prozedur zurückgegeben werden kann (meist als einzelner Eingabeparameter), damit es in der gespeicherten Prozedur überprüft werden kann. Dann können Sie feststellen, dass der @SelectTimeWert nicht länger als X Minuten vor dem aktuellen Wert von liegen darf, wenn die Operation nicht im "Vorschau" -Modus ausgeführt wird GETDATE(). Vielleicht 2 Minuten? 5 Minuten? Höchstwahrscheinlich nicht mehr als 10 Minuten. Wirft einen Fehler, wenn der DATEDIFFin MINUTES-Wert über diesem Schwellenwert liegt.

Solomon Rutzky
quelle
4

Der einfachste Ansatz ist oft der beste und ich habe nicht wirklich ein großes Problem mit der Codeduplizierung in SQL, insbesondere nicht im selben Modul. Immerhin machen die beiden Abfragen unterschiedliche Dinge. Warum also nicht 'Route 1' oder ' Keep It Simple ' wählen und nur zwei Abschnitte in der gespeicherten Prozedur haben, einen, um die Arbeit zu simulieren, die Sie erledigen müssen, und einen, um sie zu erledigen, z.

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

Dies hat den Vorteil, dass es sich selbst dokumentiert (dh es IF ... ELSEist leicht zu befolgen), dass die Komplexität gering ist (im Vergleich zum Speicherpunkt mit dem Tabellenvariablenansatz IMO) und dass es daher weniger wahrscheinlich ist, dass Fehler auftreten (großartige Stelle von @Cody).

Ich bin mir nicht sicher, ob ich es verstehe. Logischerweise sollten zwei Abfragen mit denselben Kriterien dasselbe tun. Es besteht die Möglichkeit einer Kardinalitätsinkongruenz zwischen a UPDATEund a SELECT, dies ist jedoch ein Merkmal Ihrer Verknüpfungen und Kriterien. Können Sie das näher erläutern?

Nebenbei sollten Sie die NULL/ NOT NULL-Eigenschaft sowie Ihre Tabellen und Tabellenvariablen festlegen und einen Primärschlüssel festlegen.

Ihr ursprünglicher Ansatz scheint ein bisschen zu kompliziert möglicherweise anfällige für Deadlocks sein könnte, da INSERT/ UPDATE/ DELETEOperationen erfordern höhere Schließebene als Normal SELECTs.

Ich vermute, dass Ihre Prozesse in der realen Welt komplizierter sind. Wenn Sie also der Meinung sind, dass der oben beschriebene Ansatz für sie nicht funktioniert, senden Sie uns einige weitere Beispiele.

wBob
quelle
3

Meine Bedenken lauten wie folgt.

  • Die Transaktionsbehandlung folgt nicht dem Standardmuster, in einem Block "Versuch starten / Fang starten" verschachtelt zu sein. Wenn dies eine Vorlage ist, können Sie in einer gespeicherten Prozedur mit ein paar weiteren Schritten diese Transaktion im Vorschaumodus mit noch geänderten Daten beenden.

  • Das Befolgen des Formats erhöht die Entwicklerarbeit. Wenn sie die internen Spalten ändern, müssen sie auch die Definition der Tabellenvariablen ändern, die temporäre Tabellendefinition und die Einfügungsspalten am Ende ändern. Es wird nicht populär sein.

  • Einige gespeicherte Prozeduren geben nicht jedes Mal dasselbe Datenformat zurück. Stellen Sie sich sp_WhoIsActive als gängiges Beispiel vor.

Ich habe keinen besseren Weg gefunden, aber ich denke nicht, dass Sie ein gutes Muster haben.

Cody Konior
quelle