Déclencheur pour modifier le classement de la base de données lors de la création

9

J'essaie de créer un déclencheur, de modifier le classement d'une base de données lors de sa création, mais comment puis-je attraper le nom de la base de données à utiliser dans le déclencheur?

USE master
GO
CREATE TRIGGER trg_DDL_ChangeCOllationDatabase
ON ALL SERVER
FOR CREATE_DATABASE
AS
declare @databasename varchar(200)
set @databasename =db_name()
    ALTER DATABASE @databasename COLLATE xxxxxxxxxxxxxxxxxxx
GO

De toute évidence, cela ne fonctionne pas.

Racer SQL
la source
1
Y a-t-il une raison pour laquelle vous ne pouvez pas simplement changer la base de données MODEL pour votre classement requis? - toutes les bases de données nouvellement créées utiliseraient MODEL comme modèle
Scott Hodgin
J'ai essayé mais il dit que la base de données model est une base de données système, donc je ne peux pas la changer.
Racer SQL
Ainsi, vos bases de données système seront dans un classement différent de celui de votre base de données utilisateur? Avez-vous envisagé des problèmes de classement potentiels avec les tables temporaires, etc.?
George.Palacios
Ouah, je l'ai lu il y a 5 minutes. Je n'y ai pas pensé. Ce n'est pas une bonne idée.
Racer SQL

Réponses:

8

Vous ne pouvez pas, en règle générale, émettre ALTER DATABASEau sein d'un déclencheur (ou de toute transaction contenant d'autres déclarations). Si vous tentez de le faire, vous obtiendrez l'erreur suivante:

Msg 226, niveau 16, état 6,
instruction xxx xxx ALTER DATABASE de ligne non autorisée dans la transaction multi-instruction.

La raison pour laquelle cette erreur n'a pas été rencontrée dans la réponse de @ sp_BlitzErik est le résultat du scénario de test spécifique fourni: l'erreur indiquée ci-dessus est une erreur d'exécution, tandis que l'erreur rencontrée dans sa réponse est une erreur de compilation. Cette erreur au moment de la compilation empêche l'exécution de la commande et il n'y a donc pas de "temps d'exécution". Nous pouvons voir la différence en exécutant ce qui suit:

SET NOEXEC ON;

SELECT N'g' COLLATE Latin1;

SET NOEXEC OFF;

Le lot ci-dessus entraînera une erreur, tandis que les suivants ne le seront pas:

SET NOEXEC ON;

BEGIN TRAN
CREATE TABLE #t (Col1 INT);
ALTER DATABASE CURRENT COLLATE Latin1_General_100_BIN2;
ROLLBACK TRAN;

SET NOEXEC OFF;

Cela vous laisse deux options:

  1. Validez la transaction dans le déclencheur DDL de sorte qu'il n'y ait aucune autre instruction dans la transaction. Ce n'est pas une bonne idée s'il existe plusieurs déclencheurs DDL qui peuvent être déclenchés par une CREATE DATABASEinstruction, et c'est peut-être une mauvaise idée en général, mais cela fonctionne ;-). L'astuce est que vous devez également commencer une nouvelle transaction dans le déclencheur sinon SQL Server remarquera que les valeurs de début et de fin de @@TRANCOUNTne correspondent pas et générera une erreur liée à cela. Le code ci-dessous ne fait que cela, et émet également uniquement ALTERsi le classement n'est pas celui souhaité, sinon il ignore la ALTERcommande.

    USE [master];
    GO
    CREATE TRIGGER trg_DDL_ChangeDatabaseCollation
    ON ALL SERVER
    FOR CREATE_DATABASE
    AS
    SET NOCOUNT ON;
    
    DECLARE @CollationName [sysname] = N'Latin1_General_100_BIN2',
            @SQL NVARCHAR(4000);
    
    SELECT @SQL = N'ALTER DATABASE ' + QUOTENAME(sd.[name]) + N' COLLATE ' + @CollationName
    FROM   sys.databases sd
    WHERE  sd.[name] = EVENTDATA().value(N'(/EVENT_INSTANCE/DatabaseName)[1]', N'sysname')
    AND    sd.[collation_name] <> @CollationName;
    
    IF (@SQL IS NOT NULL)
    BEGIN
      PRINT @SQL; -- DEBUG
      COMMIT TRAN; -- close existing Transaction, else will get error
      EXEC sys.sp_executesql @SQL;
      BEGIN TRAN; -- begin new Transaction, else will get different error
    END;
    ELSE
    BEGIN
      PRINT 'Collation already correct.';
    END;
    
    GO

    Testez avec:

    -- skip ALTER:
    CREATE DATABASE [tttt] COLLATE Latin1_General_100_BIN2;
    DROP DATABASE [tttt];
    
    -- perform ALTER:
    CREATE DATABASE [tttt] COLLATE SQL_Latin1_General_CP1_CI_AI;
    DROP DATABASE [tttt];
  2. Utilisez SQLCLR pour établir un régulier / externe SqlConnection, avec Enlist = false;dans la chaîne de connexion, pour émettre la ALTERcommande car cela ne fera pas partie de la transaction.

    Il semble que SQLCLR ne soit pas vraiment une option, mais pas en raison d'une limitation spécifique de SQLCLR. D'une manière ou d'une autre, taper " car cela ne fera pas partie de la transaction " directement ci-dessus n'a pas suffisamment mis en évidence le fait qu'il existe, en fait, une transaction active autour de l' CREATE DATABASEopération. Le problème ici est que même si SQLCLR peut être utilisé pour sortir de la transaction en cours, il n'y a toujours aucun moyen pour une autre session de modifier la base de données en cours de création jusqu'à ce que la transaction initiale soit validée.

    Signification, la session A crée la transaction pour la création de la base de données et le déclenchement du déclencheur. Le déclencheur, à l'aide de SQLCLR, créera la session B pour modifier la base de données qui a été créée, mais la transaction n'a pas encore été validée car elle est en attente jusqu'à la fin de la session B, ce qu'elle ne peut pas car elle attend cette transaction initiale pour Achevée. Il s'agit d'un blocage, mais il ne peut pas être détecté en tant que tel par SQL Server car il ne sait pas que la session B a été créée par quelque chose dans la session A. Ce comportement peut être vu en remplaçant la première partie de l' IFinstruction dans l'exemple ci-dessus dans # 1 avec ce qui suit:

    IF (@SQL IS NOT NULL)
    BEGIN
      /*
      PRINT @SQL; -- DEBUG
      COMMIT TRAN; -- close existing Transaction, else will get error
      EXEC sys.sp_executesql @sql;
      BEGIN TRAN; -- begin new Transaction, else will get different error
      */
      DECLARE @CMD NVARCHAR(MAX) = N'EXEC xp_cmdshell N''sqlcmd -S . -d master -E -Q "'
                                 + @SQL + N';" -t 15''';
      PRINT @CMD;
      EXEC (@CMD);
    END;
    ELSE
    ...

    Le -t 15commutateur pour SQLCMD définit le délai d'expiration de la commande / requête afin que le test n'attende pas indéfiniment avec le délai d'expiration par défaut. Mais, vous pouvez le définir pour qu'il sys.dm_exec_requestsdure plus de 15 secondes et dans une autre session, vérifiez tous les blocages en cours ;-).

  3. Mettez l'événement en file d'attente quelque part qui sera ensuite lu à partir de cette file d'attente et exécutez l' ALTER DATABASEinstruction appropriée . Cela permettra à l' CREATE DATABASEinstruction de se terminer et à sa transaction de se valider, après quoi une ALTER DATABASEinstruction peut être exécutée. Service Broker peut être utilisé ici. OU, créez une table, insérez le déclencheur dans cette table, puis demandez à un travail de l'Agent SQL Server d'appeler une procédure stockée qui lit à partir de cette table et exécute l' ALTER DATABASEinstruction, puis supprime l'enregistrement de la table de file d'attente.

TOUTEFOIS, les options ci-dessus sont principalement fournies pour aider dans les scénarios où quelqu'un a vraiment besoin de faire un certain type de ALTER DATABASEdéclencheur DDL. Dans ce scénario particulier, si vous ne voulez vraiment pas que des bases de données utilisent le classement par défaut au niveau système / instance, vous serez probablement mieux servi par:

  1. Créer une nouvelle instance avec le classement souhaité et y déplacer toutes vos bases de données utilisateur.
  2. Ou, si ce ne sont que les bases de données système qui sont du classement non idéal, il est probablement sûr de changer le classement système à partir de la ligne de commande via setup.exe (par exemple Setup.exe /Q /ACTION=Rebuilddatabase /INSTANCENAME=<instancename> /SQLCOLLATION=...; cette option recrée les bases de données système, vous aurez donc besoin pour créer des scripts pour les objets au niveau du serveur, etc. pour recréer plus tard, ainsi que pour réappliquer les correctifs, etc., FUN, FUN, FUN).
  3. Ou, pour les aventuriers de cœur, il y a l' sqlservr.exe -qoption non documentée (c'est-à-dire non prise en charge, à vos risques et périls mais pourrait très bien fonctionner) qui met à jour TOUTES les bases de données et TOUTES les colonnes (veuillez consulter Modification le classement de l'instance, des bases de données et de toutes les colonnes dans toutes les bases de données utilisateur: qu'est-ce qui pourrait éventuellement mal tourner? pour une description détaillée du comportement de cette option, ainsi que de la portée potentielle de l'impact).

    Quelle que soit l'option choisie: assurez-vous toujours d'avoir des sauvegardes de masteret msdbavant d'essayer de telles choses.

La raison pour laquelle cela vaut la peine de modifier le classement par défaut au niveau du serveur est que le classement par défaut de l'instance (c'est-à-dire au niveau du serveur) contrôle quelques domaines fonctionnels qui pourraient conduire à un comportement inattendu / incohérent, car tout le monde s'attend à ce que les opérations de chaîne fonctionnent dans le sens du classement par défaut pour toutes vos bases de données utilisateur:

  1. Classement par défaut des colonnes de chaînes dans les tables temporaires. Il s'agit d'un problème uniquement lors de la comparaison avec / Union avec d'autres colonnes de chaînes SI il existe une incompatibilité entre les deux colonnes de chaînes. Le problème ici est que lorsque vous ne spécifiez pas explicitement le classement via le COLLATEmot clé, il est beaucoup plus probable (mais non garanti) de rencontrer des problèmes.

    Ce n'est pas un problème pour le type de données XML, les variables de table ou les bases de données contenues.

  2. Métadonnées au niveau de l'instance. Par exemple, le namechamp dans sys.databasesutilisera le classement par défaut au niveau de l'instance. D'autres vues du catalogue système sont également affectées, mais je n'ai pas la liste complète.

    Les métadonnées au niveau de la base de données, telles que sys.objectset sys.indexes, ne sont pas affectées.

  3. Résolution de nom pour:
    1. variables locales (ie @variable)
    2. curseurs
    3. GOTO Étiquettes

Par exemple, si le classement au niveau de l'instance est insensible à la casse alors que le classement au niveau de la base de données est binaire (c'est-à-dire se terminant par _BINou _BIN2), la résolution de nom d'objet au niveau de la base de données sera binaire (par exemple [TableA] <> [tableA]), mais les noms de variables permettront une insensibilité à la casse (par exemple @VariableA = @variableA).

Solomon Rutzky
la source
11

Vous devez utiliser SQL dynamique et la fonction EVENTDATA () .

USE master
GO
CREATE TRIGGER trg_DDL_ChangeCOllationDatabase
ON ALL SERVER
FOR CREATE_DATABASE
AS
SET NOCOUNT ON; 
DECLARE @databasename NVARCHAR(256) = N''
DECLARE @event_data XML; 
DECLARE @sql NVARCHAR(4000) = N''

SET @event_data = EVENTDATA()

SET @databasename = @event_data.value('(/EVENT_INSTANCE/DatabaseName)[1]', 'NVARCHAR(256)') 

SET @sql += 'ALTER DATABASE ' + QUOTENAME(@databasename) + ' COLLATE al''z a-b-cee''z'

PRINT @sql

EXEC sys.sp_executesql @sql

GO

Juste sous votre collation pour mon faux .

Maintenant, quand je crée une base de données ...

CREATE DATABASE DingDong

Je reçois ce message (à partir de l'impression):

ALTER DATABASE [DingDong] COLLATE al'z ab-cee'z

Notez simplement que si d'autres bases de données (y compris tempdb) utilisent des classements différents, vous pouvez rencontrer des problèmes de comparaison des données de chaîne. Vous devez ajouter des clauses COLLATE aux comparaisons de chaînes lorsque la casse ou les accents sont importants, et même lorsqu'ils ne le sont pas, vous pouvez rencontrer des erreurs. Question connexe où j'ai rencontré un problème de code similaire ici .

Erik Darling
la source
1
@RafaelPiccinelli et Erik: juste pour info, cette réponse n'est pas tout à fait correcte. Le code ne fonctionne pas, mais l'erreur réelle est masquée en raison du test utilisant un nom de classement non valide. J'ai mis à jour ma réponse pour l'expliquer (vers le haut) car c'était trop pour un commentaire.
Solomon Rutzky
2

Vous ne pouvez pas ALTER DATABASEdans un déclencheur. Vous devrez faire preuve de créativité avec l'évaluation et la correction. Quelque chose comme:

EXEC sp_MSforeachdb N'IF EXISTS 
(
     select top 1 name from sys.databases where collation_name != 
     SQL_Latin1_General_CP1_CI_AS
)
BEGIN
    -- do something
END';

Bien que vous ne deviez pas utiliser sp_MSforeachdb .

Henrico Bekker
la source