No post anterior desta série verificamos como implementar um sistema de timeout simples, que desconecta a sessão do usuário depois de um tempo de inatividade, quando se utiliza o DataServer SQL Server. No entanto, os usuários ainda conseguiam abrir várias sessões simultâneas de cada produto. O objetivo deste post é mostrar como é possível restringir o acesso dos usuários para apenas uma sessão por produto.
Neste nosso exemplo faremos toda a implementação no banco de dados, evitando qualquer alteração no produto e ambiente. O princípio que deveremos seguir é simples: sempre que uma conexão ao banco de dados for aberta, verificar se já existe alguma conexão para o mesmo produto e mesmo usuário, porém de outra sessão. Note que é necessário validar se a conexão existente é de outra sessão, pois o DataServer SQL usa várias conexões para cada banco de dados.
Para executar um código sempre que uma nova conexão é criada, podemos utilizar uma trigger de LOGON. Esta funcionalidade está disponível no SQL Server 2005 com Service Pack 2 e superiores e no SQL Server 2008. Estas triggers são executadas após a autenticação do usuário, em um contexto de transação. Se esta transação for abortada (rollback), o usuário recebe um erro e a conexão não é estabelecida.
A trigger de exemplo abaixo leva em conta que:
- é utilizada autenticação integrada do Windows para autenticar no SQL Server;
- cada produto informa um nome de aplicação diferente para o banco de dados no momento da conexão (ex: Datasul EMS 2, Datasul EMS 5, Datasul HCM, etc);
- é utilizada a arquitetura de DataServer local.
ATENÇÃO: este código é uma ilustração dos conceitos abordados neste Post. Não há suporte para este código. Só utilize este código se entender exatamente o que ele faz e estiver satisfeito com o resultado.
CREATE TRIGGER TR_DatasulSingleUser
ON ALL SERVER
WITH EXECUTE AS 'sa'
FOR LOGON
AS
BEGIN
DECLARE @Event XML;
SET @Event = EVENTDATA();
IF EXISTS
(SELECT *
FROM sys.dm_exec_sessions s1 INNER JOIN sys.dm_exec_sessions s2
ON s1.program_name = s2.program_name AND
s1.original_login_name = s2.original_login_name AND
(s1.host_name <> s2.host_name OR
s1.host_process_id <> s2.host_process_id)
WHERE s1.is_user_process = 1
AND s2.is_user_process = 1
AND s1.client_interface_name = 'ODBC'
AND s2.client_interface_name = 'ODBC'
AND s1.program_name LIKE 'Datasul%'
AND s1.session_id = @Event.value('(/EVENT_INSTANCE/SPID)[1]', 'INT')
AND s1.original_login_name = @Event.value('(/EVENT_INSTANCE/LoginName)[1]', 'SYSNAME'))
BEGIN
ROLLBACK;
END;
END;
A função EVENTDATA() retorna um fragmento XML com as informações sobre o evento. Para um evento de LOGON, o fragmento tem este formato:
<EVENT_INSTANCE>
<EventType>LOGON</EventType>
<PostTime>2009-06-05T14:29:01.227</PostTime>
<SPID>54</SPID>
<ServerName>SERVER\instance</ServerName>
<LoginName>DOMAIN\login</LoginName>
<LoginType>Windows (NT) Login</LoginType>
<SID>SID</SID>
<ClientHost>10.20.30.40</ClientHost>
<IsPooled>0</IsPooled>
</EVENT_INSTANCE>
Como o SQL Server tem suporte nativo para trabalhar com dados XML, podemos extrair as informações do fragmento facilmente e utilizá-las para validar se existem outras sessões para o mesmo usuário. Se for detectada outra sessão, abortamos a transação do usuário (ROLLBACK). Quando a transação de LOGON é abortada, a conexão não é estabelecida e o usuário recebe um erro.
Apesar desta implementação conseguir restringir o número de sessões por usuário, a mensagem de erro apresentada ao usuário é confusa e não deixa claro qual problema realmente ocorreu. No próximo post desta série verificaremos como realizar uma implementação semelhante com código 4GL / ABL, que permite apresentar mensagens de erro mais amigáveis para o usuário.