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


27

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, ce 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 modifier GETDATE()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 des instances gérées . Il a toujours le même problème GETDATE(), il n'aide donc 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.

Réponses:


17
  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


Bien qu'en pratique cette approche soit plus compliquée qu'il n'y paraît, c'est probablement la bonne et la meilleure réponse. J'ai dû faire des tâches similaires à ces dizaines de fois et j'ai essayé toutes les approches disponibles et je n'ai rien trouvé de mieux (ou même de près, vraiment). les autres approches semblent excellentes au premier abord, mais elles deviennent rapidement un bourbier cauchemardesque de oublis et de pièges.
RBarryYoung

15

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 en utilisant AT TIME ZONE(comme vous l'avez fait ci-dessus) qui rend l'horodatage dans ce TZ spécifié (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.


s'il ne s'agissait que d'une base de données, je pourrais y penser, mais ce changement affecte de nombreuses autres applications et logiciels qui auraient besoin d'une refactorisation sérieuse. Donc, malheureusement, ce n'est pas un choix pour moi
Lamak

5
Quelle garantie aurez-vous qu'aucune des "applications et logiciels" n'utilise getdate ()? c'est-à-dire du code SQL intégré dans les applications. Si vous ne pouvez pas garantir cela, la refactorisation de la base de données pour utiliser une fonction différente entraînera simplement une incohérence.
Monsieur Magoo

@MisterMagoo Cela dépend des pratiques de la boutique, très franchement, je pense que c'est une préoccupation très mineure, et je ne vois pas qu'il faut autant de temps pour poser la question pour contourner le problème, puis pour résoudre réellement i. Il serait intéressant que cette question ne soit pas Azure, car je pourrais la pirater et vous donner plus de commentaires. Les trucs cloud craignent cependant: ils ne le supportent pas, vous devez donc changer quelque chose de votre côté. Je préférerais suivre l'itinéraire indiqué dans ma réponse et passer le temps à bien le faire. En outre, vous n'avez aucune garantie que tout fonctionnera lorsque vous passerez à Azure, comme toujours les tias.
Evan Carroll

@EvanCarroll, désolé, je viens de relire mon commentaire et je n'ai pas bien exprimé mon intention! Je voulais soutenir votre réponse (vote positif) et soulever le point que les suggestions de simplement changer l'utilisation de getdate () en getlocaldate () dans la base de données les laisseraient ouvertes aux incohérences du côté de l'application, et en plus c'est juste un coller du plâtre sur un problème plus important. 100% d'accord avec votre réponse, résoudre le problème principal est la bonne approche.
Monsieur Magoo

@MisterMagoo Je comprends votre inquiétude, mais dans ce cas, je peux garantir que les applications et les logiciels n'interagissent avec la base de données que par le biais de procédures stockées
Lamak

6

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 des espaces blancs différents / supplémentaires entre CREATEet PROCEDURE/ VIEW/ <other>. Plutôt que le REPLACEpour cela, vous préféreriez peut-être laisser le CREATEen place et en exécuter un DROPpremier, mais cela risque de laisser les sys.dependsamis hors de portée, ce qui ALTERpeut ne pas être le cas, également en cas d' ALTERéchec, vous avez au moins l'objet existant toujours en place où avec DROP+ CREATEvous pouvez ne 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-> ALTERn'interfèrent pas avec cela.

  • Vous souhaiterez tester la régression de la ou des applications entières 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 avoir des problèmes où les 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.


oui, les mises en garde sont ce qui rend cela difficile. En outre, cela ne prend pas en compte les valeurs par défaut des colonnes. Merci quand même
Lamak

Les valeurs par défaut des colonnes et d'autres contraintes peuvent également être analysées dans le sysschéma et modifiées par programme.
David Spillett

Peut-être que le remplacement par exemple CREATE OR ALTER PROCEDUREaide à résoudre certains problèmes de génération de code; il peut encore y avoir des problèmes car la définition stockée se lira CREATE PROCEDURE(trois! espaces) et cela ne correspond CREATE PROCEDUREni à CREATE OR ALTER PROCEDURE… ._.
TheConstructor

@TheConstructor - c'est à cela que je faisais référence par rapport aux "espaces blancs supplémentaires". Vous pouvez contourner ce problème en écrivant une fonction qui recherche la première CREATEqui ne se trouve pas dans un commentaire et la remplace. Je n'ai pas ceci / similaire dans le passé mais je n'ai pas le code de la fonction à portée de main pour le moment. Ou si vous pouvez garantir qu'aucune de vos définitions d'objet n'a de commentaires précédents CREATE, ignorez le problème des commentaires et recherchez et remplacez simplement la première instance de CREATE.
David Spillett

J'ai moi-même essayé cette approche à plusieurs reprises dans le passé et, dans l'ensemble, l'approche Generate-Scripts était meilleure et est presque toujours celle que j'utilise aujourd'hui, à moins que le nombre d'objets à changer ne soit relativement faible.
RBarryYoung

5

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.

entrez la description de l'image ici


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

Remarquez les sysobjects commentés Condition de colonne de type. Mon script ne modifie que proc et UDF.

Ce script va modifier tout Default Constraintce qui contientGetDate()

    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

J'ai voté pour 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 corrigé 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


UDF
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 performances avec les UDF, j'ai donc choisi d'utiliser la très longue et laide solution AT TIME ZONE.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.