Quando dici "senza usare i trigger", intendi dei trigger o dei trigger riga per riga sui tavoli?
Lo chiedo perché si potrebbe essere in grado di ottenere quello che vuoi con un uso giudizioso della CONTEXT_INFO()
funzione, ma si avrebbe bisogno di assicurare che SET CONTEXT_INFO
è stato chiamato correttamente prima le operazioni si svolgono.
Un punto da fare potrebbe essere un trigger di accesso a livello di server (ovvero non un trigger a livello di database / oggetto), in questo modo:
USE master
GO
CREATE TRIGGER tr_audit_login
ON ALL SERVER
WITH EXECUTE AS 'sa'
AFTER LOGON
AS BEGIN
BEGIN TRY
DECLARE @eventdata XML = EVENTDATA();
IF @eventdata IS NOT NULL BEGIN
DECLARE @spid INT;
DECLARE @client_host VARCHAR(64);
SET @client_host = @eventdata.value('(/EVENT_INSTANCE/ClientHost)[1]', 'VARCHAR(64)');
SET @spid = @eventdata.value('(/EVENT_INSTANCE/SPID)[1]', 'INT');
-- pack the required data into the context data binary
-- (spid is just an example of packing multiple data items in a single field: you would probably use @@SPID at the point of use, instead)
DECLARE @context_data VARBINARY(128);
SET @context_data = CONVERT(VARBINARY(4), @spid)
+ CONVERT(VARBINARY(64), @client_host);
-- persist the spid and host into session-level memory
SET CONTEXT_INFO @context_data;
END
END TRY
BEGIN CATCH
/* do better error handling here...
* logon trigger can lock all users out of server, so i am just swallowing everything
*/
DECLARE @msg NVARCHAR(4000) = ERROR_MESSAGE();
RAISERROR('%s', 10, 1, @msg) WITH LOG;
END CATCH
END
È quindi possibile aggiungere il vincolo predefinito alla tabella per archiviare il contesto (per la velocità di inserimento):
ALTER TABLE cdc.schema_table_CT
ADD ContextInfo varbinary(128) NULL DEFAULT(CONTEXT_INFO())
Una volta che hai quello, puoi interrogare quella ContextInfo
colonna con un po 'di slice-and-dice:
SELECT *
,spid = CONVERT(INT, SUBSTRING(ContextInfo, 1, 4))
,client = CONVERT(VARCHAR(64), SUBSTRING(ContextInfo, 5, 64))
FROM cdc.schema_table_CT
Tecnicamente, potresti farlo SUBSTRING
e CONVERT
cose come parte del tuo vincolo predefinito, e semplicemente archiviare lì l'IP client, ma potrebbe essere più veloce archiviare l'intero contesto lì (come è fatto su tutti INSERT
), ed estrarre solo i valori in un SELECT
quando ne hai bisogno.
Potrei essere incline a racchiudere tutto il mio SUBSTRING
e CONVERT
chiama in una funzione in linea con valori di tabella in linea singola, che farei CROSS APPLY
quando necessario. Ciò mantiene la logica di decompressione in un unico posto:
CREATE FUNCTION fn_context (
@context_info VARBINARY(128)
)
RETURNS TABLE
AS RETURN (
SELECT
spid = CONVERT(INT, SUBSTRING(@context_info, 1, 4))
,client = CONVERT(VARCHAR(64), SUBSTRING(@context_info, 5, 64))
)
GO
SELECT *
FROM cdc.schema_table_CT s
CROSS APPLY dbo.fn_context(s.ContextInfo) c
Si noti che CONTEXT_INFO
è solo un 128 byte VARBINARY
. Se hai bisogno di più dati di quanti ne puoi inserire in 128 byte, creerei una tabella per contenere tutti quei dati, inserirmi come riga per quella "sessione" nella tabella nel trigger di accesso e impostare CONTEXT_INFO
il valore chiave surrogato di quella tabella
Si noti inoltre che, poiché si tratta solo di un vincolo predefinito, è banale per un utente con privilegi adeguati sovrascrivere i dati di contesto nella tabella at-rest. Naturalmente, lo stesso vale per tutte le altre colonne anche nelle tabelle in stile "audit".
Sarebbe bello se potesse essere una colonna calcolata persistente, piuttosto che un valore predefinito, ma la CONTEXT_INFO()
funzione non è deterministica, quindi è un no-go (potresti essere in grado di usare alcuni FUNCTION
trucchi attorno a a VIEW
, ma non lo farei ).
È anche banale per quell'utente con accesso sufficiente a chiamare SET CONTEXT_INFO
se stesso e rovinare la tua giornata (ad esempio con valori falsi o iniezione memorizzata appositamente), quindi tratta i contenuti con sospetto e cura, codificali prima della visualizzazione e gestisci le eccezioni bene.
Per quanto riguarda il nome host, penso che l' ClientHost
elemento EVENTDATA()
ti dia l'indirizzo IP (o un <local machine>
indicatore). Mentre tecnicamente potresti usare CLR per eseguire ricerche DNS inverse al nome host, questi tendono ad essere troppo lenti per fare per tutti INSERT
, quindi consiglierei di non farlo.
Se è necessario disporre di un nome host, è possibile che si desideri utilizzare un processo SQL Agent per popolare periodicamente una tabella separata con i contratti di locazione correnti dal server DHCP locale o dal file della zona DNS, come processo fuori banda, e LEFT JOIN
in quello in query future (o racchiudere in uno scalare FUNCTION
per fornire un valore a un vincolo predefinito, per il punto temporale).
Ancora una volta, dovresti notare che, se l'applicazione ha qualsiasi tipo di componente pubblico, gli indirizzi IP e i nomi host sono inaffidabili (ad esempio a causa di NAT). Anche se non è rivolto al pubblico, esiste un certo componente basato sul tempo per la maggior parte delle mappe IP / hostname, che potrebbe essere necessario considerare.
Infine, prima di implementare il trigger di accesso, potrebbe essere utile attivare la connessione amministrativa dedicata del server. Se il trigger di accesso si interrompe in qualche modo, può impedire a tutti gli utenti di accedere (inclusi gli account sysadmin):
USE master
GO
-- you may want to do this, so you have a back-out if the login trigger breaks login
EXEC sp_configure 'remote admin connections', 1
GO
RECONFIGURE
GO
Se vieni bloccato, il DAC può essere utilizzato per eliminare o disabilitare il trigger di accesso:
C:\> sqlcmd -S localhost -d master -A
1> DISABLE TRIGGER tr_audit_login ON ALL SERVER
2> GO