web-dev-qa-db-fra.com

Modification de l'utilisation de GETDATE () dans toute la base de données

J'ai besoin de migrer une base de données SQL Server 2017 sur site vers une base de données Azure SQL, et je suis confronté à certains défis car il y a un certain nombre de limitations à traverser.

En particulier, étant donné qu'une base de données Azure SQL ne fonctionne qu'en heure UTC (pas de fuseau horaire) et que nous avons besoin de l'heure locale, nous devons changer l'utilisation de GETDATE() partout dans la base de données, qui s'est avéré être plus de travail que je ne l'avais prévu.

J'ai créé une fonction définie par l'utilisateur pour obtenir l'heure locale qui fonctionne correctement pour mon fuseau horaire:

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

Le problème qui me pose problème est de réellement modifier GETDATE() avec cette fonction dans chaque vue, procédure stockée, colonnes calculées, valeurs par défaut, autres contraintes, etc.

Quelle serait la meilleure façon de mettre en œuvre ce changement?

Nous sommes dans l'aperçu public de Instances gérées . Il a toujours le même problème avec GETDATE(), donc cela n'aide pas avec ce problème. Le passage à Azure est une exigence. Cette base de données est utilisée (et sera utilisée) toujours dans ce fuseau horaire.

27
Lamak
  1. Utilisez l'outil SQL Server pour exporter la définition des objets de base de données vers un fichier SQL qui doit inclure: tables, vues, déclencheurs, SP, fonctions, etc.

  2. Modifiez le fichier SQL (faites d'abord une sauvegarde) à l'aide de n'importe quel éditeur de texte qui vous permet de trouver le texte "GETDATE()" et de le remplacer par "[dbo].[getlocaldate]()"

  3. Exécutez le fichier SQL modifié dans Azure SQL pour créer vos objets de base de données ...

  4. Exécutez la migration des données.

Voici une référence de la documentation Azure: Génération de scripts pour SQL Azure

17
AMG

Quelle serait la meilleure façon de mettre en œuvre ce changement?

Je travaillerais dans l'autre sens. Convertissez tous vos horodatages de la base de données en UTC, et utilisez simplement UTC et suivez le flux. Si vous avez besoin d'un horodatage dans un autre tz, vous pouvez créer une colonne générée à l'aide de AT TIME ZONE (comme vous l'avez fait ci-dessus) qui affiche l'horodatage dans la TZ spécifiée (pour l'application). Mais, j'envisagerais sérieusement de simplement retourner UTC dans l'application et d'écrire cette logique - la logique d'affichage - dans l'application.

15
Evan Carroll

Plutôt que d'exporter, de modifier manuellement et de réexécuter, vous pouvez essayer de faire le travail directement dans la base de données avec quelque chose comme:

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

bien sûr, l'étendre pour gérer les fonctions, les déclencheurs, etc.

Il y a quelques mises en garde:

  • Vous devrez peut-être être un peu plus lumineux et gérer les espaces blancs différents/supplémentaires entre CREATE et PROCEDURE/VIEW/<other>. Plutôt que le REPLACE pour cela, vous préférerez peut-être laisser le CREATE en place et exécuter d'abord un DROP, mais cela risque de laisser sys.depends et amis en panne où ALTER ne peut pas, même si ALTER échoue, vous avez au moins l'objet existant toujours en place où avec DROP + CREATE vous ne pouvez pas.

  • Si votre code a des odeurs "intelligentes" comme la modification de son propre schéma avec TSQL ad hoc, vous devrez vous assurer que la recherche et le remplacement de CREATE-> ALTER n'interfèrent pas. avec ça.

  • Vous souhaiterez tester la régression de l'ensemble des applications après l'opération, que vous utilisiez le curseur ou les méthodes d'exportation + édition + exécution.

J'ai utilisé cette méthode pour effectuer des mises à jour similaires à l'échelle du schéma dans le passé. C'est un peu un hack et semble assez moche, mais parfois c'est le moyen le plus simple/le plus rapide.

Les valeurs par défaut et autres contraintes peuvent également être modifiées de la même manière, bien que celles-ci puissent uniquement être supprimées et recréées plutôt que modifiées. Quelque chose comme:

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

Un peu plus de plaisir auquel vous devrez peut-être faire face: si vous partitionnez en fonction du temps, ces parties peuvent également avoir besoin d'être modifiées. Bien que le partitionnement à l'heure de manière plus granulaire que le jour soit rare, vous pouvez rencontrer des problèmes où DATETIMEs sont interprétés par la fonction de partitionnement comme étant le jour précédent ou suivant selon le fuseau horaire, laissant vos partitions non alignées avec vos requêtes habituelles.

6
David Spillett

J'aime vraiment la réponse de David et j'ai voté pour une manière programmatique de faire les choses.

Mais vous pouvez essayer cela aujourd'hui pour une exécution de test dans Azure via SSMS:

Faites un clic droit sur votre base de données -> Tâches -> Générer des scripts ..

[Back Story] nous avions un DBA junior qui a mis à niveau tous nos environnements de test vers SQL 2008 R2 alors que nos environnements de production étaient à SQL 2008. C'est un changement qui me fait grincer des dents à ce jour. Pour migrer vers la production, à partir du test, nous avons dû générer des scripts dans SQL, en utilisant des scripts de génération, et dans les options avancées, nous avons utilisé l'option `` Type de données à script: schéma et données '' pour générer un fichier texte massif. Nous avons réussi à déplacer nos bases de données de test R2 vers nos serveurs SQL 2008 hérités - où une restauration de base de données vers une version inférieure n'aurait pas fonctionné. Nous avons utilisé sqlcmd pour entrer le gros fichier - car les fichiers étaient souvent trop gros pour le tampon de texte SSMS.

Ce que je dis ici, c'est que cette option fonctionnerait probablement aussi pour vous. Vous aurez juste besoin de faire une étape supplémentaire et de rechercher et remplacer getdate () par [dbo] .getlocaldate dans le fichier texte généré. (Je mettrais cependant votre fonction dans la base de données avant la migration).

(Je n'ai jamais voulu maîtriser ce pansement de restauration de base de données, mais pendant un certain temps, c'est devenu une façon de faire de facto. Et, cela a fonctionné à chaque fois.)

Si vous choisissez cette route, assurez-vous et sélectionnez le bouton Avancé et sélectionnez toutes les options dont vous avez besoin (lisez chacune) pour passer de l'ancienne base de données à la nouvelle base de données - comme les valeurs par défaut que vous avez mentionnées. Mais essayez-le quelques fois dans Azure. Je parie que vous constaterez que c'est une solution qui fonctionne - avec un minimum d'effort.

enter image description here

5
Sting

J'ai surévalué la réponse d'Evan Carrolls, car je pense que c'est la meilleure solution. Je n'ai pas réussi à convaincre mes collègues qu'ils devraient changer beaucoup de code C #, j'ai donc dû utiliser le code que David Spillett a écrit. J'ai résolu quelques problèmes avec les FDU, Dynamic SQL et les schémas (tous les codes n'utilisent pas "dbo") comme ceci:

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

et les contraintes par défaut comme ceci:

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


DF
La suggestion d'utiliser un UDF qui retourne la date et l'heure d'aujourd'hui semble agréable, mais je pense qu'il y a encore suffisamment de problèmes de performance avec les UDF, j'ai donc choisi d'utiliser le très long et laid AT Solution TIME ZONE.

1

Modifier dynamiquement tous les proc et udf pour changer la valeur

    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

Remarque commentée colonne Type de sysobjects condition.Mon script ne modifiera que proc et UDF.

Ce script va modifier tous les Default Constraint Qui contiennent GetDate()

    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
KumarHarsh