Há muitas maneiras de resolver um problema, e esse é o caso da administração de funções e status de usuário em sistemas de software. Neste artigo, você encontrará uma evolução simples dessa ideia, bem como algumas dicas úteis e exemplos de código.
Ideia básica
Na maioria dos sistemas, geralmente é necessário ter funções e status do usuário .
As funções estão relacionadas aos direitos que os usuários têm ao usar um sistema após o login bem-sucedido. Exemplos de funções são “funcionário de call center”, “gerente de call center”, “funcionário de back office”, “gerente de back office” ou “gerente”. Geralmente, isso significa que um usuário terá acesso a alguma funcionalidade se tiver a função apropriada. É aconselhável assumir que um usuário pode ter várias funções ao mesmo tempo.
Os status são muito mais rígidos e determinam se o usuário tem ou não direito de fazer login no sistema. Um usuário pode ter apenas um status de uma vez. Exemplos de status seriam:“trabalhando”, “de férias”, “de licença médica”, “contrato encerrado”.
Quando alteramos o status de um usuário, ainda podemos manter todas as funções relacionadas a esse usuário inalteradas. Isso é muito útil porque na maioria das vezes queremos alterar apenas o status do usuário. Se um usuário que trabalha como funcionário de call center sair de férias, podemos simplesmente alterar seu status para “de férias” e devolvê-lo ao status “trabalhando” quando ele voltar.
Testar funções e status durante o login nos permite decidir o que acontecerá. Por exemplo, talvez queiramos proibir o login mesmo que o nome de usuário e a senha estejam corretos. Poderíamos fazê-lo se o status atual do usuário não implicar que ele esteja trabalhando ou se o usuário não tiver nenhuma função no sistema.
Em todos os modelos abaixo, as tabelas
status
e role
são os mesmos. status
tem os campos id
e status_name
e o atributo is_active
. Se o atributo is_active
estiver definido como "True", isso significa que o usuário que possui esse status está trabalhando no momento. Por exemplo, o status “working” teria o atributo is_active
com valor True, enquanto outras (“de férias”, “de licença médica”, “contrato encerrado”) teriam valor False. A tabela de funções tem apenas dois campos:
id
e role_name
. A
user_account
table é igual a user_account
tabela apresentada neste artigo. Somente no primeiro modelo a user_account
contém dois atributos extras (role_id
e status_id
). Alguns modelos serão apresentados. Todos eles funcionam e podem ser usados, mas têm suas vantagens e desvantagens.
Modelo Simples
A primeira ideia pode ser simplesmente adicionar relacionamentos de chave estrangeira à
user_account
tabela, referenciando em tabelas status
e role
. Ambos role_id
e status_id
são obrigatórios. Isso é bastante simples de projetar e também de lidar com dados com consultas, mas tem algumas desvantagens:
-
Não mantemos nenhum dado histórico (ou futuro).
Quando alteramos o status ou função, simplesmente atualizamosstatus_id
erole_id
nauser_account
tabela. Isso funcionará bem por enquanto, então, quando fizermos uma alteração, ela refletirá no sistema. Tudo bem se não precisarmos saber como os status e as funções mudaram historicamente. Além disso, há um problema em que não podemos adicionar futuro função ou status sem adicionar tabelas extras a este modelo. Uma situação em que provavelmente gostaríamos de ter essa opção é quando sabemos que alguém estará de férias a partir da próxima segunda-feira. Outro exemplo é quando temos um novo funcionário; talvez queiramos inserir seu status e função agora e que se torne válido em algum momento no futuro.
Há também uma complicação caso tenhamos eventos agendados que usam funções e status. Eventos que preparam dados para o próximo dia útil geralmente são executados enquanto a maioria dos usuários não usa o sistema (por exemplo, durante a noite). Portanto, se alguém não trabalhar amanhã, teremos que esperar até o final do dia atual e alterar suas funções e status conforme apropriado. Por exemplo, se tivermos funcionários que trabalham atualmente e têm a função de “funcionário de call center”, eles receberão uma lista de clientes para os quais devem ligar. Se alguém por engano tiver esse status e função, ele também receberá seus clientes e teremos que gastar tempo corrigindo isso.
-
O usuário pode ter apenas uma função por vez.
Geralmente, os usuários devem poder ter mais de uma função no sistema. Talvez no momento em que você está projetando o banco de dados não haja necessidade de algo assim. Tenha em mente que podem ocorrer mudanças no fluxo de trabalho/processo. Por exemplo, em algum momento o cliente pode decidir mesclar duas funções em uma. Uma solução possível é criar uma nova função e atribuir a ela todas as funcionalidades das funções anteriores. A outra solução (se os usuários puderem ter mais de uma função) seria que o cliente simplesmente atribua ambas as funções aos usuários que precisam delas. Claro que essa segunda solução é mais prática e dá ao cliente a capacidade de ajustar o sistema às suas necessidades mais rapidamente (o que não é suportado por este modelo).
Por outro lado, este modelo também tem uma grande vantagem sobre os outros. É simples e, portanto, as consultas para alterar status e funções também seriam simples. Além disso, uma consulta que verifica se o usuário tem direitos de login no sistema é muito mais simples do que em outros casos:
select user_account.id, user_account.role_id from user_account left join status on user_account.status_id = status.id where status.is_user_working = True and user_account.user_name = @user_name and user_account.password_hash_algorithm = @password;
@user_name e @password são variáveis de um formulário de entrada enquanto a consulta retorna o id do usuário e o role_id que ele possui. Nos casos em que nome_usuário ou senha não são válidos, o par nome_usuário e senha não existe ou o usuário tem um status atribuído que não está ativo, a consulta não retornará nenhum resultado. Dessa forma, podemos proibir o login.
Este modelo pode ser usado nos casos em que:
- temos certeza de que não haverá mudanças no processo que exijam que os usuários tenham mais de uma função
- não precisamos rastrear alterações de funções/status no histórico
- não esperamos ter muita administração de função/status.
Componente de tempo adicionado
Se precisarmos rastrear a função e o histórico de status de um usuário, devemos adicionar muitos relacionamentos entre a
user_account
e role
e a user_account
e status
. Claro que removeremos role_id
e status_id
da user_account
tabela. As novas tabelas no modelo são user_has_role
e user_has_status
e todos os campos neles, exceto os horários de término, são obrigatórios. A tabela
user_has_role
contém dados sobre todas as funções que os usuários já tiveram no sistema. A chave alternativa é (user_account_id
, role_id
, role_start_time
) porque não faz sentido atribuir a mesma função ao mesmo tempo a um usuário mais de uma vez. A tabela
user_has_status
contém dados sobre todos os status que os usuários já tiveram no sistema. A chave alternativa aqui é (user_account_id
, status_start_time
) porque um usuário não pode ter dois status que comecem exatamente ao mesmo tempo. A hora de início não pode ser nula porque quando inserimos uma nova função/status, sabemos o momento a partir do qual ela começará. A hora de término pode ser nula caso não saibamos quando a função/status terminaria (por exemplo, a função é válida a partir de amanhã até que algo aconteça no futuro).
Além de ter um histórico completo, agora podemos adicionar status e funções no futuro. Mas isso cria complicações porque temos que verificar a sobreposição quando fazemos uma inserção ou atualização.
Por exemplo, o usuário pode ter apenas um status por vez. Antes de inserir um novo status, temos que comparar a hora de início e a hora de término de um novo status com todos os status existentes para esse usuário no banco de dados. Podemos usar uma consulta como esta:
select * from user_has_status where user_has_status.user_account_id = @user_account_id and ( # test if @start_time included in interval of some previous status (user_has_status.status_start_time <= @start_time and ifnull(user_has_status.status_end_time, "2200-01-01") >= @start_time) or # test if @end_time included in interval of some previous status (user_has_status.status_start_time <= @end_time and ifnull(user_has_status.status_end_time, "2200-01-01") >= ifnull(@end_time, "2199-12-31")) or # if @end_time is null we cannot have any statuses after @start_time (@end_time is null and user_has_status.status_start_time >= @start_time) or # new status "includes" old satus (@start_time <= user_has_status.status_start_time <= @end_time) (user_has_status.status_start_time >= @start_time and user_has_status.status_start_time <= ifnull(@end_time, "2199-12-31")) )
@start_time
e @end_time
são variáveis que contêm a hora de início e a hora de término de um status que queremos inserir e @user_account_id
é o ID do usuário para o qual o inserimos. @end_time
pode ser nulo e devemos tratá-lo na consulta. Para este propósito, os valores nulos são testados com o ifnull()
função. Se o valor for nulo, um valor de data alto será atribuído (alto o suficiente para que, quando alguém perceber um erro na consulta, já estejamos fora :). A consulta verifica todas as combinações de hora de início e hora de término para um novo status em comparação com a hora de início e a hora de término dos status existentes. Se a consulta retornar algum registro, temos sobreposição com status existentes e devemos proibir a inserção do novo status. Também seria bom gerar um erro personalizado.
Se quisermos verificar a lista de funções e status atuais (direitos do usuário), simplesmente testamos usando a hora de início e a hora de término.
select user_account.id, user_has_role.id from user_account left join user_has_role on user_has_role.user_account_id = user_account.id left join user_has_status on user_account.id = user_has_status.user_account_id left join status on user_has_status.status_id = status.id where user_account.user_name = @user_name and user_account.password_hash_algorithm = @password and user_has_role.role_start_time <= @time and ifnull(user_has_role.role_end_time,"2200-01-01") >= @time and user_has_status.status_start_time <= @time and ifnull(user_has_status.status_end_time,"2200-01-01") >= @time and status.is_user_working = True
@user_name
e @password
são variáveis do formulário de entrada enquanto @time
pode ser definido como Agora(). Quando um usuário tenta fazer login, queremos verificar seus direitos naquele momento. O resultado é uma lista de todas as funções que um usuário possui no sistema caso o nome_do_usuário e a senha correspondam e o usuário tenha atualmente um status ativo. Se o usuário tiver um status ativo, mas nenhuma função atribuída, a consulta não retornará nada.
Essa consulta é mais simples que a da seção 3 e esse modelo nos permite ter um histórico de status e papéis. Além disso, podemos gerenciar status e funções para o futuro e tudo funcionará bem.
Modelo final
Esta é apenas uma ideia de como o modelo anterior poderia ser alterado se quiséssemos melhorar o desempenho. Como um usuário pode ter apenas um status ativo por vez, podemos adicionar
status_id
na user_account
tabela (current_status_id
). Dessa forma, podemos testar o valor desse atributo e não precisaremos ingressar no user_has_status
tabela. A consulta modificada ficaria assim:select user_account.id, user_has_role.id from user_account left join user_has_role on user_has_role.user_account_id = user_account.id left join status on user_account.current_status_id = status.id where user_account.user_name = @user_name and user_account.password_hash_algorithm = @password and user_has_role.role_start_time <= @time and ifnull(user_has_role.role_end_time,"2200-01-01") >= @time and status.is_user_working = True
Obviamente, isso simplifica a consulta e leva a um melhor desempenho, mas há um problema maior que precisaria ser resolvido. O
current_status_id
na user_account
tabela deve ser verificada e alterada se necessário nas seguintes situações:- em cada inserção/atualização/exclusão em
user_has_status
tabela - todos os dias em um evento agendado, devemos verificar se o status de alguém mudou (status ativo atual expirou ou/e algum status futuro se tornou ativo) e atualizá-lo adequadamente
Seria sensato salvar valores que as consultas usarão com frequência. Dessa forma, evitaremos fazer as mesmas verificações repetidas vezes e dividiremos o trabalho. Aqui, evitaremos participar do
user_has_status
tabela e faremos alterações em current_status_id
apenas quando eles acontecem (inserir/atualizar/excluir) ou quando o sistema não está em uso (eventos agendados geralmente são executados quando a maioria dos usuários não usa o sistema). Talvez, neste caso, não ganharíamos muito com current_status_id
mas veja isso como uma ideia que pode ajudar em situações semelhantes.