Esistono molti modi per risolvere un problema, e questo è il caso dell'amministrazione dei ruoli e degli stati degli utenti nei sistemi software. In questo articolo troverai una semplice evoluzione di quell'idea, nonché alcuni suggerimenti utili ed esempi di codice.
Idea di base
Nella maggior parte dei sistemi, di solito è necessario avere ruoli e gli stati degli utenti .
I ruoli sono correlati ai diritti che gli utenti hanno durante l'utilizzo di un sistema dopo aver effettuato correttamente l'accesso. Esempi di ruoli sono "impiegato del call center", "responsabile del call center", "impiegato del back office", "responsabile del back office" o "manager". In genere ciò significa che un utente avrà accesso ad alcune funzionalità se ha il ruolo appropriato. È saggio presumere che un utente possa avere più ruoli contemporaneamente.
Gli stati sono molto più severi e determinano se l'utente ha i diritti per accedere al sistema o meno. Un utente può avere solo uno stato Al tempo. Esempi di stati potrebbero essere:"lavorare", "in vacanza", "in congedo per malattia", "contratto terminato".
Quando modifichiamo lo stato di un utente, possiamo comunque mantenere invariati tutti i ruoli relativi a quell'utente. Questo è molto utile perché la maggior parte delle volte vogliamo cambiare solo lo stato dell'utente. Se un utente che lavora come dipendente del call center va in ferie, possiamo semplicemente cambiare il suo stato in "in ferie" e riportarlo allo stato "lavorativo" quando torna.
Testare ruoli e stati durante l'accesso ci consente di decidere cosa accadrà. Ad esempio, forse vogliamo vietare l'accesso anche se nome utente e password sono corretti. Potremmo farlo se lo stato attuale dell'utente non implica che stia lavorando o se l'utente non ha alcun ruolo nel sistema.
In tutti i modelli riportati di seguito, le tabelle status
e role
sono gli stessi.
Tabella status
ha i campi id
e status_name
e l'attributo is_active
. Se l'attributo is_active
è impostato su "True", ciò significa che l'utente che ha quello stato sta attualmente lavorando. Ad esempio, lo stato "funzionante" avrebbe l'attributo is_active
con valore Vero, mentre altri (“in ferie”, “in congedo per malattia”, “contratto scaduto”) avrebbero valore Falso.
La tabella dei ruoli ha solo due campi:id
e role_name
.
Il user_account
la tabella è la stessa del user_account
tabella presentata in questo articolo. Solo nel primo modello fa il user_account
la tabella contiene due attributi extra (role_id
e status_id
).
Verranno presentati alcuni modelli. Tutti funzionano e possono essere utilizzati, ma hanno i loro vantaggi e svantaggi.
Modello semplice
La prima idea potrebbe essere quella di aggiungere semplicemente relazioni di chiave esterna a user_account
tabella, facendo riferimento alle tabelle status
e role
. Entrambi role_id
e status_id
sono obbligatori.
Questo è abbastanza semplice da progettare e anche per gestire i dati con le query, ma presenta alcuni svantaggi:
-
Non conserviamo alcun dato storico (o futuro).
Quando cambiamo lo stato o il ruolo, aggiorniamo semplicemente
status_id
erole_id
neluser_account
tavolo. Per ora funzionerà bene, quindi quando apportiamo una modifica si rifletterà nel sistema. Questo va bene se non abbiamo bisogno di sapere come sono cambiati storicamente gli stati e i ruoli. Inoltre c'è un problema in quanto non possiamo aggiungere futuro ruolo o stato senza aggiungere tabelle aggiuntive a questo modello. Una situazione in cui probabilmente vorremmo avere questa opzione è quando sappiamo che qualcuno sarà in vacanza a partire da lunedì prossimo. Un altro esempio è quando abbiamo un nuovo dipendente; forse vogliamo entrare nel suo status e nel suo ruolo ora e affinché diventi valido ad un certo punto in futuro.C'è anche una complicazione nel caso in cui abbiamo eventi programmati che utilizzano ruoli e stati. Gli eventi che preparano i dati per il giorno lavorativo successivo di solito vengono eseguiti mentre la maggior parte degli utenti non utilizza il sistema (ad esempio durante la notte). Quindi, se qualcuno non lavorerà domani, dovremo aspettare fino alla fine della giornata in corso e quindi cambiare i suoi ruoli e lo stato a seconda dei casi. Ad esempio, se abbiamo dipendenti che attualmente lavorano e hanno il ruolo di "impiegato del call center", riceveranno un elenco di clienti che devono chiamare. Se qualcuno per errore ha avuto quello status e ruolo, otterrà anche i suoi clienti e dovremo dedicare del tempo a correggerlo.
-
L'utente può avere un solo ruolo alla volta.
In genere gli utenti dovrebbero poter avere più di un ruolo nel sistema. Forse nel momento in cui stai progettando il database non è necessario qualcosa del genere. Tieni presente che potrebbero verificarsi cambiamenti nel flusso di lavoro/processo. Ad esempio, a volte il cliente potrebbe decidere di unire due ruoli in uno solo. Una possibile soluzione è creare un nuovo ruolo e assegnargli tutte le funzionalità dei ruoli precedenti. L'altra soluzione (se gli utenti possono avere più di un ruolo) sarebbe che il client assegni semplicemente entrambi i ruoli agli utenti che ne hanno bisogno. Naturalmente quella seconda soluzione è più pratica e dà al cliente la possibilità di adattare il sistema alle sue esigenze più velocemente (cosa non supportata da questo modello).
D'altra parte, questo modello ha anche un grande vantaggio rispetto ad altri. È semplice e quindi anche le query per modificare stati e ruoli sarebbero semplici. Inoltre, una query che controlla se l'utente dispone dei diritti per accedere al sistema è molto più semplice che in altri casi:
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 sono variabili da un modulo di input mentre la query restituisce l'id dell'utente e il role_id che ha. Nei casi in cui nome_utente o password non sono validi, la coppia nome_utente e password non esiste o l'utente ha uno stato assegnato che non è attivo, la query non restituirà alcun risultato. In questo modo possiamo vietare l'accesso.
Questo modello potrebbe essere utilizzato nei casi in cui:
- siamo sicuri che non ci sarebbero cambiamenti in corso che richiedano agli utenti di avere più di un ruolo
- non è necessario tenere traccia dei ruoli/cambiamenti di stato nella cronologia
- Non ci aspettiamo di avere molta amministrazione del ruolo/stato.
Componente temporale aggiunta
Se dobbiamo tenere traccia del ruolo di un utente e della cronologia dello stato, dobbiamo aggiungere molte a molte relazioni tra l'user_account
e role
e il user_account
e status
. Ovviamente rimuoveremo role_id
e status_id
dal user_account
tavolo. Le nuove tabelle nel modello sono user_has_role
e user_has_status
e tutti i campi in essi contenuti, eccetto gli orari di fine, sono obbligatori.
La tabella user_has_role
contiene dati su tutti i ruoli che gli utenti hanno mai avuto nel sistema. La chiave alternativa è (user_account_id
, role_id
, role_start_time
) perché non ha senso assegnare lo stesso ruolo contemporaneamente a un utente più di una volta.
La tabella user_has_status
contiene dati su tutti gli stati che gli utenti hanno mai avuto nel sistema. La chiave alternativa qui è (user_account_id
, status_start_time
) perché un utente non può avere due stati che iniziano esattamente nello stesso momento.
L'ora di inizio non può essere nulla perché quando inseriamo un nuovo ruolo/stato, conosciamo il momento da cui partirà. L'ora di fine può essere nulla nel caso in cui non sappiamo quando il ruolo/lo stato sarebbe terminato (ad es. il ruolo è valido da domani fino a quando non succede qualcosa in futuro).
Oltre ad avere una cronologia completa, ora possiamo aggiungere stati e ruoli in futuro. Ma questo crea complicazioni perché dobbiamo verificare la sovrapposizione quando eseguiamo un inserimento o un aggiornamento.
Ad esempio, l'utente può avere un solo stato alla volta. Prima di inserire un nuovo stato, dobbiamo confrontare l'ora di inizio e l'ora di fine di un nuovo stato con tutti gli stati esistenti per quell'utente nel database. Possiamo usare una query come questa:
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
sono variabili contenenti l'ora di inizio e l'ora di fine di uno stato che vogliamo inserire e @user_account_id
è l'ID utente per il quale lo inseriamo. @end_time
può essere nullo e dobbiamo gestirlo nella query. A tale scopo, i valori null vengono verificati con ifnull()
funzione. Se il valore è nullo, viene assegnato un valore di data alto (abbastanza alto che quando qualcuno nota un errore nella query saremo lontani da tempo :). La query controlla tutte le combinazioni di ora di inizio e ora di fine per un nuovo stato rispetto all'ora di inizio e all'ora di fine degli stati esistenti. Se la query restituisce dei record, allora abbiamo una sovrapposizione con gli stati esistenti e dovremmo vietare l'inserimento del nuovo stato. Inoltre sarebbe bello generare un errore personalizzato.
Se vogliamo controllare l'elenco dei ruoli e degli stati correnti (diritti utente) testiamo semplicemente utilizzando l'ora di inizio e l'ora di fine.
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
sono variabili dal modulo di input mentre @time
potrebbe essere impostato su Now(). Quando un utente tenta di accedere, vogliamo verificare i suoi diritti in quel momento. Il risultato è un elenco di tutti i ruoli che un utente ha nel sistema nel caso in cui nome_utente e password corrispondano e l'utente abbia attualmente uno stato attivo. Se l'utente ha uno stato attivo ma nessun ruolo assegnato, la query non restituirà nulla.
Questa query è più semplice di quella della sezione 3 e questo modello ci consente di avere una cronologia di stati e ruoli. Inoltre, possiamo gestire stati e ruoli per il futuro e tutto funzionerà correttamente.
Modello finale
Questa è solo un'idea di come il modello precedente potrebbe essere cambiato se volessimo migliorare le prestazioni. Poiché un utente può avere un solo stato attivo alla volta, potremmo aggiungere status_id
nel user_account
tabella (current_status_id
). In questo modo, possiamo testare il valore di quell'attributo e non dovremo unirci a user_has_status
tavolo. La query modificata sarebbe simile a questa:
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
Ovviamente questo semplifica la query e porta a prestazioni migliori, ma c'è un problema più grande che dovrebbe essere risolto. Il current_status_id
nel user_account
la tabella dovrebbe essere controllata e modificata se necessario nelle seguenti situazioni:
- su ogni inserimento/aggiornamento/eliminazione in
user_has_status
tabella - ogni giorno in un evento programmato dovremmo controllare se lo stato di qualcuno è cambiato (lo stato attualmente attivo è scaduto o/e qualche stato futuro è diventato attivo) e aggiornarlo di conseguenza
Sarebbe saggio salvare i valori che le query utilizzeranno frequentemente. In questo modo eviteremo di ripetere gli stessi controlli più e più volte e di dividere il lavoro. Qui eviteremo di entrare in user_has_status
tabella e apporteremo modifiche su current_status_id
solo quando si verificano (inserisci/aggiorna/elimina) o quando il sistema non è molto utilizzato (gli eventi pianificati di solito vengono eseguiti quando la maggior parte degli utenti non utilizza il sistema). Forse in questo caso non guadagneremmo molto da current_status_id
ma considera questa come un'idea che può aiutare in situazioni simili.