Procedura memorizzata del database con una "modalità di anteprima"


15

Un modello abbastanza comune nell'applicazione di database con cui lavoro è la necessità di creare una procedura memorizzata per un report o un'utilità con una "modalità di anteprima". Quando una tale procedura esegue gli aggiornamenti, questo parametro indica che i risultati dell'azione devono essere restituiti, ma la procedura non deve effettivamente eseguire gli aggiornamenti al database.

Un modo per ottenere ciò è semplicemente scrivere ifun'istruzione per il parametro e avere due blocchi di codice completi; uno dei quali aggiorna e restituisce i dati e l'altro restituisce semplicemente i dati. Ma questo non è auspicabile a causa della duplicazione del codice e di un livello relativamente basso di fiducia nel fatto che i dati di anteprima siano in realtà un riflesso accurato di ciò che accadrebbe con un aggiornamento.

L'esempio seguente tenta di sfruttare i punti di salvataggio e le variabili delle transazioni (che non sono interessati dalle transazioni, a differenza delle tabelle temporanee che sono) per utilizzare solo un singolo blocco di codice per la modalità di anteprima come modalità di aggiornamento live.

Nota: i rollback delle transazioni non sono un'opzione poiché questa chiamata di procedura può essere nidificata in una transazione. Questo è testato su 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

Sto cercando feedback su questo codice e modello di progettazione e / o se esistono altre soluzioni allo stesso problema in diversi formati.

Risposte:


12

Esistono diversi difetti in questo approccio:

  1. Il termine "anteprima" può essere abbastanza fuorviante nella maggior parte dei casi, a seconda della natura dei dati su cui si opera (e che cambia da operazione a operazione). Cosa fare per garantire che i dati attuali su cui operare siano nello stesso stato tra il momento in cui vengono raccolti i dati di "anteprima" e quando l'utente ritorna 15 minuti dopo - dopo aver preso un caffè, uscire per fumare, camminare in giro per il blocco, rientrando e controllando qualcosa su eBay - e ti rendi conto che non hanno fatto clic sul pulsante "OK" per eseguire effettivamente l'operazione e quindi finalmente fa clic sul pulsante?

    Hai un limite di tempo per procedere con l'operazione dopo la generazione dell'anteprima? O forse un modo per determinare che i dati sono nello stesso stato al momento della modifica rispetto al SELECTmomento iniziale ?

  2. Questo è un punto minore in quanto il codice di esempio avrebbe potuto essere fatto in fretta e non rappresenterebbe un vero caso d'uso, ma perché dovrebbe esserci una "Anteprima" per INSERTun'operazione? Ciò potrebbe avere senso quando si inseriscono più righe tramite qualcosa di simile INSERT...SELECTe potrebbe esserci un numero variabile di righe inserite, ma ciò non ha molto senso per un'operazione singleton.

  3. questo è indesiderabile a causa di ... un livello relativamente basso di fiducia nel fatto che i dati di anteprima siano in realtà un riflesso accurato di ciò che accadrebbe con un aggiornamento.

    Da dove viene esattamente questo "basso livello di fiducia"? Sebbene sia possibile aggiornare un numero di righe diverso da quello mostrato per un SELECTquando più tabelle sono JOIN e c'è una duplicazione di righe in un set di risultati, questo non dovrebbe essere un problema qui. Tutte le righe che dovrebbero essere interessate da un UPDATEsono selezionabili da sole. In caso di mancata corrispondenza, la query viene eseguita in modo errato.

    E quelle situazioni in cui esiste una duplicazione dovuta a una tabella JOINed che corrisponde a più righe nella tabella che verranno aggiornate non sono situazioni in cui verrebbe generata una "Anteprima". E se c'è un'occasione in cui questo è il caso, allora deve essere spiegato all'utente che vengono aggiornati un sottoinsieme del rapporto che si ripete all'interno del rapporto in modo che non sembri essere un errore se qualcuno è solo guardando il numero di righe interessate.

  4. Per completezza (anche se le altre risposte hanno menzionato questo), non stai usando il TRY...CATCHcostrutto, quindi potresti incorrere facilmente in problemi durante l'annidamento di queste chiamate (anche se non usi i punti di salvataggio e anche se non utilizzi le transazioni). Si prega di consultare la mia risposta alla seguente domanda, qui su DBA.SE, per un modello che gestisce le transazioni tra chiamate di procedure memorizzate nidificate:

    Ci viene richiesto di gestire la transazione nel codice C # e nella procedura memorizzata

  5. ANCHE SE i problemi sopra indicati sono stati spiegati, c'è ancora un difetto critico: per il breve periodo di tempo in cui l'operazione viene eseguita (cioè prima del ROLLBACK), qualsiasi query di lettura sporca (query che utilizza WITH (NOLOCK)o SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED) può acquisire dati che non c'è un momento dopo. Mentre chiunque utilizzi query di lettura sporca dovrebbe già essere a conoscenza di questo e aver accettato tale possibilità, operazioni come questa aumentano notevolmente le possibilità di introdurre anomalie dei dati che sono molto difficili da eseguire il debug (significato: quanto tempo vuoi dedicare cercando di trovare un problema che non ha una causa diretta apparente?).

  6. Un modello come questo degrada anche le prestazioni del sistema aumentando i blocchi eliminando più blocchi e generando più attività del registro delle transazioni. (Vedo ora che @MartinSmith ha anche menzionato questi 2 problemi in un commento sulla domanda.)

    Inoltre, se ci sono trigger sulle tabelle in fase di modifica, potrebbe essere necessario un po 'di elaborazione aggiuntiva (letture CPU e fisiche / logiche) che non è necessaria. I trigger aumenterebbero inoltre le possibilità di anomalie dei dati derivanti da letture errate.

  7. Relativamente al punto notato sopra - aumento dei blocchi - l'uso della Transazione aumenta la probabilità di imbattersi in deadlock, specialmente se sono coinvolti i Trigger.

  8. Un problema meno grave che dovrebbe riguardare solo lo scenario meno probabile delle INSERToperazioni: i dati di "Anteprima" potrebbero non essere gli stessi di quelli inseriti in relazione ai valori di colonna determinati dai DEFAULTVincoli ( Sequences/ NEWID()/ NEWSEQUENTIALID()) e IDENTITY.

  9. Non è necessario l'overhead aggiuntivo della scrittura del contenuto della variabile tabella nella tabella temporanea. Il ROLLBACKnon interesserebbe i dati nella tabella variabile (che è il motivo per cui lei ha detto che si stava utilizzando le variabili di tabella in primo luogo), quindi sarebbe più sensato semplicemente SELECT FROM @output_to_return;alla fine, e quindi non si preoccupano neppure di creare il Temporary Tavolo.

  10. Nel caso in cui questa sfumatura di punti di salvataggio non sia nota (difficile da capire dal codice di esempio in quanto mostra solo una singola procedura memorizzata): è necessario utilizzare nomi univoci di punti di salvataggio in modo che l' ROLLBACK {save_point_name}operazione si comporti come previsto. Se riutilizzi i nomi, un ROLLBACK eseguirà il rollback del punto di salvataggio più recente con quel nome, che potrebbe non trovarsi allo stesso livello di annidamento da cui ROLLBACKviene chiamato. Vedere il primo blocco di codice di esempio nella seguente risposta per vedere questo comportamento in azione: Transazione in una procedura memorizzata

Ciò a cui si riduce è:

  • Fare una "Anteprima" non ha molto senso per le operazioni rivolte all'utente. Lo faccio frequentemente per le operazioni di manutenzione in modo da poter vedere cosa verrà eliminato / Garbage Collected se procedo con l'operazione. Aggiungo un parametro opzionale chiamato @TestModee faccio IFun'istruzione che o fa un altro SELECTquando @TestMode = 1fa il DELETE. A volte aggiungo il @TestModeparametro alle Stored procedure richiamate dall'applicazione in modo che io (e altri) possa eseguire test semplici senza influire sullo stato dei dati, ma questo parametro non viene mai utilizzato dall'applicazione.

  • Nel caso in cui ciò non fosse chiaro nella parte superiore di "problemi":

    Se hai bisogno / vuoi una modalità "Anteprima" / "Test" per vedere cosa dovrebbe essere influenzato se l'istruzione DML dovesse essere eseguita, allora NON usare Transazioni (cioè il BEGIN TRAN...ROLLBACKmodello) per farlo. È un modello che, nella migliore delle ipotesi, funziona davvero solo su un sistema a utente singolo, e nemmeno una buona idea in quella situazione.

  • Ripetere la maggior parte della query tra i due rami IFdell'istruzione presenta un potenziale problema di necessità di aggiornarli entrambi ogni volta che è necessario apportare una modifica. Tuttavia, le differenze tra le due query sono in genere abbastanza facili da comprendere in una revisione del codice e facili da correggere. D'altra parte, problemi come differenze di stato e letture sporche sono molto più difficili da trovare e risolvere. E il problema della riduzione delle prestazioni del sistema è impossibile da risolvere. Dobbiamo riconoscere e accettare che SQL non è un linguaggio orientato agli oggetti e l'incapsulamento / riduzione del codice duplicato non era un obiettivo di progettazione di SQL come in molti altri linguaggi.

    Se la query è abbastanza lunga / complessa, è possibile incapsularla in una funzione con valori di tabella incorporata. Quindi puoi fare un semplice SELECT * FROM dbo.MyTVF(params);per la modalità "Anteprima" e UNISCITI ai valori chiave per la modalità "fallo". Per esempio:

    UPDATE tab
    SET    tab.Col2 = tvf.ColB
           ...
    FROM   dbo.Table tab
    INNER JOIN dbo.MyTVF(params) tvf
            ON tvf.ColA = tab.Col1;
  • Se questo è uno scenario di report, come è stato menzionato, l'esecuzione del report iniziale è "Anteprima". Se qualcuno vuole cambiare qualcosa che vede sul rapporto (uno stato forse), ciò non richiede un'anteprima aggiuntiva poiché l'aspettativa è di cambiare i dati attualmente visualizzati.

    Se l'operazione deve forse modificare un importo dell'offerta di una determinata% o regola aziendale, ciò può essere gestito nel livello di presentazione (JavaScript?).

  • Se hai davvero bisogno di fare una "Anteprima" per un'operazione rivolta all'utente finale , devi prima catturare lo stato dei dati (forse un hash di tutti i campi nel set di risultati per le UPDATEoperazioni o i valori chiave per DELETEoperazioni), quindi, prima di eseguire l'operazione, confronta le informazioni sullo stato acquisito con le informazioni correnti - all'interno di una Transazione facendo un HOLDblocco sul tavolo in modo che nulla cambi dopo aver fatto questo confronto - e se ci sono QUALSIASI differenza, lancia un errore e fare un ROLLBACKanziché procedere con UPDATEo DELETE.

    Per rilevare le differenze nelle UPDATEoperazioni, un'alternativa al calcolo di un hash nei campi pertinenti sarebbe quella di aggiungere una colonna di tipo ROWVERSION . Il valore di un ROWVERSIONtipo di dati cambia automaticamente ogni volta che si modifica una riga. Se tu avessi una colonna del genere, la faresti SELECTinsieme agli altri dati "Anteprima", e poi li passeresti al passaggio "sicuro, vai avanti e fai l'aggiornamento" insieme ai valori chiave e ai valori chiave cambiare. Dovresti quindi confrontare i ROWVERSIONvalori passati da "Anteprima" con i valori correnti (per ogni chiave) e procedere solo con UPDATEif ALLdei valori corrispondenti. Il vantaggio qui è che non è necessario calcolare un hash che ha il potenziale, anche se improbabile, per falsi negativi e impiega un po 'di tempo ogni volta che lo fai SELECT. D'altra parte, il ROWVERSIONvalore viene incrementato automaticamente solo quando viene modificato, quindi non dovrai mai preoccuparti di nulla. Tuttavia, il ROWVERSIONtipo è di 8 byte, che possono essere sommati quando si ha a che fare con molte tabelle e / o molte righe.

    Esistono vantaggi e svantaggi di ciascuno di questi due metodi per gestire il rilevamento di uno stato incoerente correlato alle UPDATEoperazioni, quindi è necessario determinare quale metodo ha più "pro" che "con" per il proprio sistema. Ma in entrambi i casi, è possibile evitare un ritardo tra la generazione dell'anteprima e l'esecuzione dell'operazione causando comportamenti al di fuori delle aspettative dell'utente finale.

  • Se si sta eseguendo una modalità "Anteprima" rivolta all'utente finale, oltre a catturare lo stato dei record al momento della selezione, passando e controllare al momento della modifica, includere un DATETIMEfor SelectTimee popolare tramite GETDATE()o qualcosa di simile. Passalo al livello dell'app in modo che possa essere restituito alla procedura memorizzata (probabilmente come singolo parametro di input) in modo che possa essere verificato nella Stored Procedure. Quindi è possibile determinare se SE l'operazione non è la modalità "Anteprima", quindi il @SelectTimevalore deve essere non più di X minuti prima del valore corrente di GETDATE(). Forse 2 minuti? Cinque minuti? Molto probabilmente non più di 10 minuti. Generare un errore se DATEDIFFin MINUTES supera quella soglia.


4

L'approccio più semplice è spesso il migliore e in realtà non ho un grosso problema con la duplicazione del codice in SQL, soprattutto non nello stesso modulo. Dopo tutte e due le domande stanno facendo cose diverse. Quindi perché non prendere 'Route 1' o Keep It Simple e avere solo due sezioni nel proc memorizzato, uno per simulare il lavoro che devi fare e uno per farlo, ad esempio qualcosa del genere:

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

Questo ha il vantaggio di essere auto-documentante (es IF ... ELSE è facile da seguire), bassa complessità (rispetto al punto di salvataggio con approccio IMO a tabella variabile), quindi meno probabilità di avere bug (ottimo spot di @Cody).

Per quanto riguarda il tuo punto sulla scarsa fiducia, non sono sicuro di capire. Logicamente due query con gli stessi criteri dovrebbero fare la stessa cosa. Esiste la possibilità di una discrepanza di cardinalità tra an UPDATEe aSELECT , ma sarebbe una caratteristica dei tuoi join e criteri. Puoi spiegare ulteriormente?

A parte, dovresti impostare il NULLNOT NULL proprietà / e le tue tabelle e variabili di tabella, considera l'impostazione di una chiave primaria.

Il tuo approccio originale sembra un po ' troppo complicato potrebbe essere più soggetto a deadlock, come INSERT/ UPDATE/DELETE operazioni richiedono livelli di blocco più elevati rispetto a quelli normali SELECTs.

Sospetto che i tuoi proc nel mondo reale siano più complicati, quindi se ritieni che l'approccio sopra non funzionerà per loro, rispondi con alcuni altri esempi.


3

Le mie preoccupazioni sono le seguenti.

  • La gestione delle transazioni non segue il modello standard di nidificazione in un blocco Inizia prova / Inizia cattura. Se si tratta di un modello, in una procedura memorizzata con alcuni altri passaggi è possibile uscire da questa transazione in modalità anteprima con i dati ancora modificati.

  • Seguire il formato aumenta il lavoro degli sviluppatori. Se cambiano le colonne interne, devono anche modificare la definizione della variabile della tabella, quindi modificare la definizione della tabella temporanea, quindi modificare le colonne di inserimento alla fine. Non sarà popolare.

  • Alcune stored procedure non restituiscono sempre lo stesso formato di dati; pensa a sp_WhoIsActive come un esempio comune.

Non ho fornito un modo migliore per farlo, ma non credo che quello che hai sia un buon modello.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.