Trigger per modificare le regole di confronto del database durante la creazione


9

Sto cercando di creare un trigger, per modificare le regole di confronto di un database durante la sua creazione, ma come posso catturare il nome del database da utilizzare all'interno del trigger?

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

Ovviamente, questo non funziona.


1
C'è un motivo per cui non puoi semplicemente cambiare il database MODEL con le regole di confronto richieste? - tutti i database appena creati utilizzerebbero MODELLO come modello
Scott Hodgin,

L'ho provato ma dice che il database dei modelli è un database di sistema, quindi non posso cambiarlo.
Racer SQL,

Quindi i database di sistema saranno in regole di confronto diverse dai database dell'utente? Hai preso in considerazione potenziali problemi di confronto con tabelle temporanee ecc.?
George.Palacios,

Wow, sì, l'ho letto 5 minuti fa. Non ci ho pensato. Questa non è una buona idea.
Racer SQL,

Risposte:


8

In linea generale, non è possibile emettere ALTER DATABASEun trigger (o qualsiasi Transazione che contiene altre dichiarazioni in esso). Se si tenta di, si otterrà il seguente errore:

Messaggio 226, livello 16, stato 6, riga xxxx
Istruzione ALTER DATABASE non consentita nella transazione multiistruzione.

Il motivo per cui questo errore non è stato riscontrato nella risposta di @ sp_BlitzErik è il risultato del caso di test specifico fornito: l'errore mostrato sopra è un errore di runtime, mentre l'errore riscontrato nella sua risposta è un errore di compilazione. Questo errore di compilazione impedisce l'esecuzione del comando e quindi non esiste un "run-time". Possiamo vedere la differenza eseguendo quanto segue:

SET NOEXEC ON;

SELECT N'g' COLLATE Latin1;

SET NOEXEC OFF;

Il batch di cui sopra genererà un errore, mentre il seguente non lo farà:

SET NOEXEC ON;

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

SET NOEXEC OFF;

Questo ti lascia con due opzioni:

  1. Esegui il commit della transazione all'interno del trigger DDL in modo tale che non vi siano altre dichiarazioni nella transazione. Questa non è una buona idea se ci sono più trigger DDL che possono essere attivati ​​da CREATE DATABASEun'istruzione, ed è probabilmente una cattiva idea in generale, ma funziona ;-). Il trucco è che devi anche iniziare una nuova Transazione nel Trigger altrimenti SQL Server noterà che i valori di inizio e fine per @@TRANCOUNTnon corrispondono e genererà un errore correlato a quello. Il codice qui sotto fa proprio questo, ed emette anche solo ALTERse il Collation non è quello desiderato, altrimenti salta il ALTERcomando.

    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

    Prova con:

    -- 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. Utilizzare SQLCLR per stabilire un normale / esterno SqlConnection, con Enlist = false;nella stringa di connessione, per emettere il ALTERcomando in quanto non farà parte della transazione.

    Sembra che SQLCLR non sia veramente un'opzione, anche se non a causa di alcuna limitazione specifica di SQLCLR. In qualche modo, digitando "in quanto ciò non farà parte della Transazione " direttamente sopra non è stato sufficientemente evidenziato il fatto che esiste, di fatto, una Transazione attiva attorno CREATE DATABASEall'operazione. Il problema qui è che mentre SQLCLR può essere utilizzato per uscire dalla Transazione corrente, non è ancora possibile che un'altra Sessione modifichi il Database attualmente in fase di creazione fino a quando la Transazione iniziale non viene confermata.

    Significato, la sessione A crea la transazione per la creazione del database e l'attivazione del trigger. Il trigger, utilizzando SQLCLR, creerà la sessione B per modificare il database che è stato creato, ma la transazione non è stata ancora impegnata poiché è in attesa fino al completamento della sessione B, cosa che non può perché è in attesa che la transazione iniziale venga completare. Questo è un deadlock, ma non può essere rilevato come tale da SQL Server poiché non sa che la Sessione B è stata creata da qualcosa all'interno della Sessione A. Questo comportamento può essere visto sostituendo la prima parte IFdell'istruzione nell'esempio sopra in # 1 con il seguente:

    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
    ...

    L' opzione-t 15 per SQLCMD imposta il timeout comando / query in modo che il test non attenda per sempre con il timeout predefinito. Ma puoi impostarlo in modo che sia più lungo di 15 secondi e in un'altra sessione controlla sys.dm_exec_requestsper vedere tutti gli adorabili blocchi in corso ;-).

  3. Metti in coda l'evento da qualche parte che poi leggerà da quella coda ed eseguirà l' ALTER DATABASEistruzione appropriata . Ciò consentirà il CREATE DATABASEcompletamento dell'istruzione e il commit della sua transazione, dopodiché ALTER DATABASEsarà possibile eseguire un'istruzione. Service Broker può essere utilizzato qui. OPPURE, creare una tabella, fare in modo che il trigger venga inserito in quella tabella, quindi un processo di SQL Server Agent chiama una stored procedure che legge da quella tabella ed esegue l' ALTER DATABASEistruzione e quindi rimuove il record dalla tabella delle code.

TUTTAVIA, le opzioni di cui sopra sono principalmente fornite per aiutare in scenari in cui qualcuno ha davvero bisogno di fare un qualche tipo ALTER DATABASEall'interno di un trigger DDL. In questo particolare scenario, se davvero non si desidera che nessun database utilizzi le regole di confronto predefinite a livello di sistema / istanza, è probabile che si verifichino i migliori servizi:

  1. Creazione di una nuova istanza con le regole di confronto desiderate e spostamento di tutti i database utente su di essa.
  2. Oppure, se sono solo i database di sistema che fanno parte delle regole di confronto non ideali, è probabilmente sicuro modificare le regole di confronto del sistema dalla riga di comando tramite setup.exe (ad esempio Setup.exe /Q /ACTION=Rebuilddatabase /INSTANCENAME=<instancename> /SQLCOLLATION=...; questa opzione ricrea i DB di sistema, quindi sarà necessario per eseguire lo script di oggetti a livello di server, ecc. per ricrearli in seguito, oltre a riapplicare patch, ecc., FUN, FUN, FUN).
  3. Oppure, per gli avventurosi, c'è l' sqlservr.exe -qopzione non documentata (cioè non supportata, usa-a-il-tuo-rischio-ma-potrebbe-molto-funzionante) che aggiorna TUTTI i DB e TUTTE le colonne (vedi Modifica la raccolta delle istanze, dei database e di tutte le colonne in tutti i database degli utenti: cosa potrebbe andare storto? per una descrizione dettagliata del comportamento di questa opzione, nonché del potenziale ambito di impatto).

    Indipendentemente dall'opzione scelta: assicurarsi sempre di disporre di backup mastere msdbprima di tentare tali operazioni.

Il motivo per cui varrebbe la pena modificare la Fascicolazione predefinita a livello di server è che la Fascicolazione predefinita dell'istanza (ovvero a livello di server) controlla alcune aree funzionali che potrebbero portare a comportamenti imprevisti / incoerenti, poiché tutti si aspettano che le operazioni sulle stringhe funzionino sulla falsariga delle regole di confronto predefinite per tutti i database utente:

  1. Fascicolazione predefinita per colonne di stringhe in tabelle temporanee. Questo è un problema solo se confrontato con / Unione con altre colonne di stringhe SE c'è una discrepanza tra le due colonne di stringhe. Il problema qui è che quando non si specifica esplicitamente la raccolta tramite la COLLATEparola chiave, è molto più probabile (sebbene non garantito) incorrere in problemi.

    Questo non è un problema per il tipo di dati XML, le variabili di tabella o i database contenuti.

  2. Meta-dati a livello di istanza. Ad esempio, il namecampo in sys.databasesutilizzerà le regole di confronto predefinite a livello di istanza. Anche le altre visualizzazioni del catalogo di sistema sono interessate, ma non ho l'elenco completo.

    I metadati a livello di database, come sys.objectse sys.indexes, non sono interessati.

  3. Risoluzione del nome per:
    1. variabili locali (ie @variable)
    2. cursori
    3. GOTO etichette

Ad esempio, se le regole di confronto a livello di istanza non fanno distinzione tra maiuscole e minuscole mentre le regole di confronto a livello di database sono binarie (ovvero terminano in _BINo _BIN2), la risoluzione dei nomi degli oggetti a livello di database sarà binaria (ad esempio [TableA] <> [tableA]), ma i nomi delle variabili consentiranno di distinguere tra maiuscole e minuscole (ad es @VariableA = @variableA.).


11

Dovresti usare SQL dinamico e la funzione 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

Basta sub nella tua raccolta per il mio falso .

Ora quando creo un database ...

CREATE DATABASE DingDong

Ricevo questo messaggio (dalla stampa):

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

Si noti che se altri database (incluso tempdb) utilizzano regole di confronto diverse, è possibile riscontrare problemi nel confronto dei dati di stringa. Dovresti aggiungere clausole COLLATE ai confronti di stringhe in cui il case o gli accenti contano, e anche quando non lo fanno puoi colpire errori. Domanda correlata in cui mi sono imbattuto in un problema di codice simile qui .


1
@RafaelPiccinelli ed Erik: solo FYI, questa risposta non è del tutto corretta. Il codice non funziona, ma l'errore reale viene mascherato a causa del test che utilizza un nome di confronto non valido. Ho aggiornato la mia risposta per spiegare (verso l'alto) perché era troppo per un commento.
Solomon Rutzky,

2

Non puoi ALTER DATABASEinnescare. Devi essere creativo con la valutazione e la correzione. Qualcosa di simile a:

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';

Anche se non dovresti usare sp_MSforeachdb .

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.