Modifica dell'uso di GETDATE () nell'intero database


27

Devo migrare un database SQL Server 2017 locale in un database SQL di Azure e sto affrontando alcune sfide poiché ci sono alcune limitazioni da superare.

In particolare, poiché un database SQL di Azure funziona solo nell'ora UTC (senza fusi orari) e abbiamo bisogno dell'ora locale, dobbiamo cambiare l'uso di GETDATE() ovunque nel database, che ha dimostrato di essere più lavoro di quanto mi aspettassi.

Ho creato una funzione definita dall'utente per ottenere l'ora locale che funziona correttamente per il mio fuso orario:

CREATE FUNCTION [dbo].[getlocaldate]()
RETURNS datetime
AS
BEGIN
    DECLARE @D datetimeoffset;
    SET @D = CONVERT(datetimeoffset, SYSDATETIMEOFFSET()) AT TIME ZONE 'Pacific SA Standard Time';
    RETURN(CONVERT(datetime,@D));
END

Il problema con cui ho problemi è effettivamente cambiare GETDATE()con questa funzione in ogni vista, procedura memorizzata, colonne calcolate, valori predefiniti, altri vincoli, ecc.

Quale sarebbe il modo migliore per attuare questo cambiamento?

Siamo nell'anteprima pubblica di Managed Instances . Ha ancora lo stesso problema con GETDATE(), quindi non aiuta con questo problema. Il passaggio ad Azure è un requisito. Questo database viene utilizzato (e verrà utilizzato) sempre in questo fuso orario.

Risposte:


17
  1. Utilizzare lo strumento SQL Server per esportare la definizione degli oggetti del database in un file SQL che dovrebbe includere: tabelle, viste, trigger, SP, funzioni e così via

  2. Modifica il file SQL (esegui prima un backup) utilizzando qualsiasi editor di testo che ti consenta di trovare il testo "GETDATE()"e sostituirlo con"[dbo].[getlocaldate]()"

  3. Esegui il file SQL modificato in Azure SQL per creare gli oggetti del database ...

  4. Eseguire la migrazione dei dati.

Qui hai un riferimento dalla documentazione azzurra: Generazione di script per SQL Azure


Sebbene in pratica questo approccio sia più complicato di quanto sembri, è probabilmente la risposta corretta e migliore. Ho dovuto svolgere attività simili a tante decine di volte e ho provato tutti gli approcci disponibili e non ho trovato niente di meglio (o addirittura vicino, davvero). gli altri approcci sembrano grandi all'inizio, ma diventano rapidamente un pantano da incubo di sviste e trucchi.
RBarryYoung,

15

Quale sarebbe il modo migliore per attuare questo cambiamento?

Vorrei lavorare al contrario. Converti tutti i tuoi timestamp nel database in UTC e usa semplicemente UTC e segui il flusso. Se hai bisogno di un timestamp in una diversa tz, puoi creare una colonna generata usando AT TIME ZONE(come hai fatto sopra) che esegue il rendering del timestamp nella TZ specificata (per l'app). Ma prenderei seriamente in considerazione il fatto che UTC sia tornato all'app e che abbia scritto quella logica - la logica di visualizzazione - nell'app.


se fosse solo una questione di database, potrei considerarlo, ma quella modifica riguarda molte altre app e software che avrebbero bisogno di un serio refactoring. Quindi, purtroppo, non è una scelta per me
Lamak

5
Quale garanzia avrai che nessuna delle "app e software" usi getdate ()? cioè il codice sql incorporato nelle app. Se non è possibile garantirlo, il refactoring del database per utilizzare una funzione diversa porterà solo a un'incoerenza.
Mago Magoo,

@MisterMagoo Dipende dalle pratiche del negozio, francamente penso che questa sia una preoccupazione molto minore, e non riesco a vedere che ci vuole tanto tempo per porre la domanda per aggirare il problema e poi per risolvere effettivamente i. Sarebbe interessante se questa domanda non fosse Azure, perché potrei hackerarla e darti più feedback. Il cloud fa schifo però: non lo supportano, quindi devi cambiare qualcosa dalla tua parte. Preferirei seguire il percorso indicato nella mia risposta e dedicare del tempo a farlo nel modo giusto. Inoltre, non hai garanzie che qualcosa funzioni quando ti sposti ad Azure, come sempre.
Evan Carroll,

@EvanCarroll, scusa se ho appena riletto il mio commento e non ho espresso bene il mio intento! Intendevo supportare la tua risposta (votata) e sollevare il punto che i suggerimenti di cambiare semplicemente l'uso di getdate () per getlocaldate () nel database li avrebbero lasciati aperti alle incoerenze dal lato dell'app, e inoltre è solo un attaccare il cerotto su un problema più grande. Il 100% concorda con la tua risposta, risolvere il problema principale è l'approccio giusto.
Mago Magoo,

@MisterMagoo Capisco la tua preoccupazione, ma in questo caso, posso garantire che app e software interagiscano con il database solo attraverso procedure memorizzate
Lamak,

6

Invece di esportare, modificare manualmente ed eseguire nuovamente, potresti provare a fare il lavoro direttamente nel database con qualcosa del tipo:

DECLARE C CURSOR FOR
        SELECT sm.definition, so.type
        FROM   sys.objects so
        JOIN   sys.all_sql_modules sm ON sm.object_id = so.object_id
        WHERE  so.type IN ('P', 'V')
        ORDER BY so.name
DECLARE @SQL NVARCHAR(MAX), @ojtype NVARCHAR(MAX)
OPEN C
FETCH NEXT FROM C INTO @SQL, @ojtype
WHILE @@FETCH_STATUS = 0 BEGIN
    IF @objtype = 'P' SET @SQL = REPLACE(@SQL, 'CREATE PROCEDURE', 'ALTER PROCEDURE') 
    IF @objtype = 'V' SET @SQL = REPLACE(@SQL, 'CREATE VIEW'     , 'ALTER VIEW'     ) 
    SET @SQL = REPLACE(@SQL, 'GETDATE()', '[dbo].[getlocaldate]()') 
    --PRINT @SQL
    EXEC (@SQL)
    FETCH NEXT FROM C INTO @SQL, @ojtype
END
CLOSE C
DEALLOCATE C

ovviamente estenderlo per gestire funzioni, trigger e così via.

Ci sono alcuni avvertimenti:

  • Potrebbe essere necessario essere un po 'più luminoso e gestire spazi bianchi diversi / extra tra CREATEe PROCEDURE/ VIEW/ <other>. Piuttosto che REPLACEper quello che potresti preferire invece lasciare il CREATEposto ed eseguire un DROPprimo, ma questo rischia di andarsene sys.dependse amici fuori dal kilter dove ALTERnon può, anche se ALTERfallisci hai almeno l'oggetto esistente ancora al posto dove con DROP+ CREATEpuoi non.

  • Se il tuo codice ha odori "intelligenti" come la modifica del suo schema con TSQL ad hoc, dovrai assicurarti che la ricerca e la sostituzione di CREATE-> ALTERnon interferiscano con quello.

  • Avrai voglia di testare la regressione dell'intera (e) applicazione (i) dopo l'operazione, sia che usi il cursore o esporti + modifica + esegui metodi.

Ho usato questo metodo per effettuare aggiornamenti simili a livello di schema in passato. È un po 'un trucco e si sente piuttosto brutto, ma a volte è il modo più semplice / veloce.

Anche i valori predefiniti e altri vincoli possono essere modificati in modo simile, anche se possono essere eliminati e ricreati anziché modificati. Qualcosa di simile a:

DECLARE C CURSOR FOR
        SELECT AlterDefaultSQL = 'ALTER TABLE [' +st.name+ '] DROP CONSTRAINT [' + si.name + '];'
                               + CHAR(10)
                               + 'ALTER TABLE [' +st.name+ '] ADD CONSTRAINT [' + si.name + '] DEFAULT '+REPLACE(si.definition, 'GETDATE()', '[dbo].[getlocaldate]()')+' FOR '+sc.name+';'
        FROM   sys.tables st
        JOIN   sys.default_constraints si ON si.parent_object_id = st.object_id
        JOIN   sys.columns sc ON sc.default_object_id = si.object_id
DECLARE @SQL NVARCHAR(MAX)
OPEN C
FETCH NEXT FROM C INTO @SQL
WHILE @@FETCH_STATUS = 0 BEGIN
    --PRINT @SQL
    EXEC (@SQL)
    FETCH NEXT FROM C INTO @SQL
END
CLOSE C
DEALLOCATE C

Qualche divertimento in più che potresti dover affrontare: se stai partizionando in tempo, anche quelle parti potrebbero aver bisogno di essere modificate. Mentre il partizionamento puntuale in modo più granulare quindi di giorno è raro che potresti avere problemi in cui i DATETIMEs vengono interpretati dalla funzione di partizionamento come il giorno precedente o successivo a seconda del fuso orario, lasciando le tue partizioni disallineate con le solite query.


sì, le avvertenze sono ciò che rende questo difficile. Inoltre, questo non tiene conto dei valori predefiniti della colonna. Grazie comunque
Lamak il

I valori predefiniti delle colonne e altri vincoli possono anche essere scansionati nello sysschema e modificati a livello di programmazione.
David Spillett,

Forse la sostituzione, ad esempio, CREATE OR ALTER PROCEDUREaiuta a risolvere alcuni problemi di generazione del codice; ci possono ancora essere problemi mentre la definizione memorizzata leggerà CREATE PROCEDURE(tre! spazi) e questo non è abbinato da CREATE PROCEDURECREATE OR ALTER PROCEDURE… ._.
TheConstructor il

@TheConstructor - questo è ciò a cui mi riferivo per scrivere "extra spazi bianchi". Puoi aggirare questo problema scrivendo una funzione che scansiona il primo CREATEche non si trova all'interno di un commento e lo sostituisce. Non ho questo / simile in passato, ma non ho il codice della funzione a portata di mano in questo momento per pubblicare. Oppure, se puoi garantire che nessuna delle definizioni degli oggetti abbia commenti precedenti CREATE, ignora il problema dei commenti e trova e sostituisci la prima istanza di CREATE.
David Spillett,

Ho provato questo approccio da me stesso numerose volte in passato e, a conti fatti, l'approccio Generate-Scripts era migliore ed è quasi sempre quello che uso oggi a meno che il numero di oggetti da modificare non sia relativamente piccolo.
RBarryYoung,

5

Mi piace molto la risposta di David e l'ho votata per un modo programmatico di fare le cose.

Ma puoi provarlo oggi stesso per un'esecuzione di test in Azure tramite SSMS:

Fare clic con il tasto destro del mouse sul database -> Attività -> Genera script ..

[Back Story] avevamo un DBA junior che ha aggiornato tutti i nostri ambienti di test a SQL 2008 R2 mentre i nostri ambienti di produzione erano a SQL 2008. È un cambiamento che mi fa rabbrividire fino ai nostri giorni. Per migrare alla produzione, dal test, abbiamo dovuto generare script all'interno di SQL, utilizzando script di generazione, e nelle opzioni avanzate abbiamo usato l'opzione "Tipo di dati per lo script: schema e dati" per generare un enorme file di testo. Siamo riusciti a spostare con successo i nostri database di test R2 sui nostri server SQL 2008 legacy, dove un ripristino del database su una versione precedente non avrebbe funzionato. Abbiamo usato sqlcmd per inserire file di grandi dimensioni, poiché i file erano spesso troppo grandi per il buffer di testo SSMS.

Quello che sto dicendo qui è che questa opzione probabilmente funzionerebbe anche per te. Devi solo fare un ulteriore passaggio e cercare e sostituire getdate () con [dbo] .getlocaldate nel file di testo generato. (Vorrei mettere la tua funzione nel database prima della migrazione però).

(Non ho mai voluto essere esperto in questo cerotto di un ripristino di database, ma per un po 'è diventato un modo defacto di fare le cose. E ha funzionato ogni volta.)

Se si sceglie questa route, accertarsi di selezionare il pulsante Avanzate e selezionare tutte le opzioni necessarie (leggere ciascuna) per passare dal vecchio database al nuovo database, come le impostazioni predefinite menzionate. Ma dagli alcune prove in Azure. Scommetto che scoprirai che questa è una soluzione che funziona, con un minimo sforzo.

inserisci qui la descrizione dell'immagine


1

Modifica dinamicamente tutto proc e udf per cambiare valore

    DECLARE @Text   NVARCHAR(max), 
        @spname NVARCHAR(max), 
        @Type   CHAR(5), 
        @Sql    NVARCHAR(max) 
DECLARE @getobject CURSOR 

SET @getobject = CURSOR 
FOR SELECT sc.text, 
           so.NAME, 
           so.type 
    FROM   sys.syscomments sc 
           INNER JOIN sysobjects so 
                   ON sc.id = so.id 
    WHERE  sc.[text] LIKE '%getdate()%' 

--and type in('P','FN') 
OPEN @getobject 

FETCH next FROM @getobject INTO @Text, @spname, @Type 

WHILE @@FETCH_STATUS = 0 
  BEGIN 
      IF ( @Type = 'P' 
            OR @Type = 'FN' ) 
        SET @Text = Replace(@Text, 'getdate', 'dbo.getlocaldate') 

      SET @Text = Replace(@Text, 'create', 'alter') 

      EXECUTE Sp_executesql 
        @Text 

      PRINT @Text 

      --,@spname,@Type 
      FETCH next FROM @getobject INTO @Text, @spname, @Type 
  END 

CLOSE @getobject 

DEALLOCATE @getobject  

 

    CREATE PROCEDURE [dbo].[Testproc1] 
AS 
    SET nocount ON; 

  BEGIN 
      DECLARE @CurDate DATETIME = Getdate() 
  END

Si noti la condizione della colonna Tipo di oggetto commentato. Il mio script modificherà solo proc e UDF.

Questo script modificherà tutto ciò Default Constraintche contieneGetDate()

    DECLARE @TableName      VARCHAR(300), 
        @constraintName VARCHAR(300), 
        @colName        VARCHAR(300), 
        @Sql            NVARCHAR(max) 
DECLARE @getobject CURSOR 

SET @getobject = CURSOR 
FOR SELECT ds.NAME, 
           sc.NAME AS colName, 
           so.NAME AS Tablename 
    --,ds.definition 
    FROM   sys.default_constraints ds 
           INNER JOIN sys.columns sc 
                   ON ds.object_id = sc.default_object_id 
           INNER JOIN sys.objects so 
                   ON so.object_id = ds.parent_object_id 
    WHERE  definition LIKE '%getdate()%' 

OPEN @getobject 

FETCH next FROM @getobject INTO @constraintName, @colName, @TableName 

WHILE @@FETCH_STATUS = 0 
  BEGIN 
      SET @Sql = 'ALTER TABLE ' + @TableName 
                 + ' DROP CONSTRAINT ' + @constraintName + '; ' 
                 + Char(13) + Char(10) + '           ' + Char(13) + Char(10) + '' 
      SET @Sql = @Sql + ' ALTER TABLE ' + @TableName 
                 + ' ADD CONSTRAINT ' + @constraintName 
                 + '          DEFAULT dbo.GetLocaledate() FOR ' 
                 + @colName + ';' + Char(13) + Char(10) + '          ' + Char(13) 
                 + Char(10) + '' 

      PRINT @Sql 

      EXECUTE sys.Sp_executesql 
        @Sql 

      --,@spname,@Type 
      FETCH next FROM @getobject INTO @constraintName, @colName, @TableName 
  END 

CLOSE @getobject 

DEALLOCATE @getobject   

1

Ho votato a favore di Evan Carrolls, poiché penso che questa sia la soluzione migliore . Non sono stato in grado di convincere i miei colleghi che avrebbero dovuto cambiare molto codice C #, quindi ho dovuto usare il codice scritto da David Spillett. Ho risolto un paio di problemi con UDF, Dynamic SQL e Schemi (non tutti i codici usano "dbo") in questo modo:

DECLARE C CURSOR LOCAL STATIC FOR
        SELECT sm.definition, so.type
        FROM   sys.objects so
        JOIN   sys.all_sql_modules sm ON sm.object_id = so.object_id
        WHERE  so.type IN ('P', 'V')
        AND CHARINDEX('getdate()', sm.definition) > 0
        ORDER BY so.name

DECLARE @SQL NVARCHAR(MAX), @objtype NVARCHAR(MAX)
OPEN C
WHILE 1=1 BEGIN
    FETCH NEXT FROM C INTO @SQL, @objtype
    IF @@FETCH_STATUS <> 0 BREAK

    IF @objtype = 'P' SET @SQL = REPLACE(@SQL, 'CREATE PROCEDURE', 'ALTER PROCEDURE') 
    IF @objtype = 'P' SET @SQL = REPLACE(@SQL, 'CREATE   PROCEDURE', 'ALTER PROCEDURE') /* when you write "create or alter proc" */
    IF @objtype = 'V' SET @SQL = REPLACE(@SQL, 'CREATE VIEW'     , 'ALTER VIEW'     ) 
    IF CHARINDEX('getdate())''', @sql) > 0 BEGIN  /* when dynamic SQL is used */
        IF CHARINDEX('utl.getdate())''', @sql) = 0 SET @SQL = REPLACE(@SQL, 'GETDATE()', 'utl.getdate()') 
    end
    ELSE begin
        SET @SQL = REPLACE(@SQL, 'GETDATE()', 'CONVERT(DATETIME, CONVERT(datetimeoffset,  SYSDATETIME()) AT TIME ZONE ''Central Europe Standard Time'')') 
    end
    EXEC dbo.LongPrint @String = @sql    
    EXEC (@SQL)
END
CLOSE C
DEALLOCATE C

e i vincoli predefiniti come questo:

DECLARE C CURSOR LOCAL STATIC FOR
        SELECT AlterDefaultSQL = 'ALTER TABLE [' +sch.name+ '].[' +st.name+ '] DROP CONSTRAINT [' + si.name + '];'
                               + CHAR(10)
                               + 'ALTER TABLE [' +sch.name+ '].[' +st.name+ '] ADD CONSTRAINT [' + si.name + '] DEFAULT '+REPLACE(si.definition, 'GETDATE()', 'CONVERT(DATETIME, CONVERT(datetimeoffset,  SYSDATETIME()) AT TIME ZONE ''Central Europe Standard Time'')')+' FOR '+sc.name+';'
        FROM   sys.tables st
        JOIN   sys.default_constraints si ON si.parent_object_id = st.object_id
        JOIN   sys.columns sc ON sc.default_object_id = si.object_id
        INNER JOIN sys.schemas sch ON sch.schema_id = st.schema_id
        WHERE CHARINDEX('getdate()', si.definition) > 0
        ORDER BY st.name, sc.name

DECLARE @SQL NVARCHAR(MAX)
OPEN C
WHILE 1=1 BEGIN
    FETCH NEXT FROM C INTO @SQL
    IF @@FETCH_STATUS <> 0 BREAK

    EXEC dbo.LongPrint @String = @sql  
    EXEC (@SQL)
    FETCH NEXT FROM C INTO @SQL
END
CLOSE C
DEALLOCATE C


UDF
Il suggerimento di utilizzare un UDF che restituisce la data e l'ora odierne sembra carino, ma penso che ci siano ancora abbastanza problemi di prestazioni con gli UDF, quindi ho scelto di utilizzare la soluzione molto lunga e brutta AT TIME ZONE.

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.