Istnieje wiele sposobów na rozwiązanie problemu i tak jest w przypadku administrowania rolami i statusami użytkowników w systemach oprogramowania. W tym artykule znajdziesz prostą ewolucję tego pomysłu, a także kilka przydatnych wskazówek i przykładów kodu.
Podstawowy pomysł
W większości systemów zwykle istnieje potrzeba posiadania roli i statusy użytkowników .
Role są powiązane z prawami które użytkownicy mają podczas korzystania z systemu po pomyślnym zalogowaniu. Przykładami ról są „pracownik call center”, „menedżer call center”, „pracownik back office”, „menedżer back office” lub „menedżer”. Ogólnie oznacza to, że użytkownik będzie miał dostęp do niektórych funkcji, jeśli będzie miał odpowiednią rolę. Rozsądnie jest założyć, że użytkownik może jednocześnie pełnić wiele ról.
Statusy są znacznie bardziej rygorystyczne i określają, czy użytkownik ma uprawnienia do logowania się do systemu, czy nie. Użytkownik może mieć tylko jeden status na czas. Przykładowe statusy to:„pracujący”, „na urlopie”, „na zwolnieniu chorobowym”, „umowa zakończona”.
Gdy zmienimy status użytkownika, nadal możemy zachować wszystkie role związane z tym użytkownikiem bez zmian. Jest to bardzo pomocne, ponieważ przez większość czasu chcemy zmienić tylko status użytkownika. Jeśli użytkownik, który pracuje jako pracownik call center, wyjeżdża na urlop, możemy po prostu zmienić jego status na „na urlopie” i przywrócić go do statusu „pracujący”, gdy wróci.
Testowanie ról i statusów podczas logowania pozwala nam decydować, co się stanie. Na przykład może chcemy zabronić logowania, nawet jeśli nazwa użytkownika i hasło są poprawne. Moglibyśmy to zrobić, jeśli obecny status użytkownika nie sugeruje, że pracuje lub jeśli użytkownik nie ma żadnej roli w systemie.
We wszystkich modelach podanych poniżej tabele status
i role
są takie same.
Tabela status
ma pola id
i status_name
oraz atrybut is_active
. Jeśli atrybut is_active
jest ustawiony na „True”, co oznacza, że użytkownik, który ma ten status, aktualnie pracuje. Na przykład status „pracujący” miałby atrybut is_active
z wartością Prawda, podczas gdy inne („na wakacjach”, „na zwolnieniu lekarskim”, „kontrakt zakończony”) miałyby wartość Fałsz.
Tabela ról ma tylko dwa pola:id
i role_name
.
user_account
tabela jest taka sama jak user_account
tabela prezentowana w tym artykule. Tylko w pierwszym modelu user_account
tabela zawiera dwa dodatkowe atrybuty (role_id
i status_id
).
Zaprezentowanych zostanie kilka modeli. Wszystkie działają i mogą być używane, ale mają swoje wady i zalety.
Prosty model
Pierwszym pomysłem może być to, że po prostu dodamy relacje klucza obcego do user_account
tabela, odwołanie do tabel status
i role
. Oba role_id
i status_id
są obowiązkowe.
Jest to dość proste do zaprojektowania, a także do obsługi danych za pomocą zapytań, ale ma kilka wad:
-
Nie przechowujemy żadnych danych historycznych (ani przyszłych).
Kiedy zmieniamy status lub rolę, po prostu aktualizujemy
status_id
irole_id
wuser_account
stół. Na razie będzie to działać dobrze, więc kiedy wprowadzimy zmianę, zostanie to odzwierciedlone w systemie. Jest to w porządku, jeśli nie musimy wiedzieć, jak historycznie zmieniały się statusy i role. Jest też problem polegający na tym, że nie możemy dodać przyszłości rolę lub status bez dodawania dodatkowych tabel do tego modelu. Jedną z sytuacji, w której prawdopodobnie chcielibyśmy mieć taką możliwość, jest sytuacja, gdy wiemy, że ktoś będzie na wakacjach począwszy od przyszłego poniedziałku. Innym przykładem jest sytuacja, w której mamy nowego pracownika; być może chcemy wejść w jego status i rolę teraz i żeby stało się ważne w pewnym momencie w przyszłości.Istnieje również komplikacja w przypadku, gdy mamy zaplanowane wydarzenia które wykorzystują role i statusy. Zdarzenia przygotowujące dane na kolejny dzień roboczy są zwykle uruchamiane, gdy większość użytkowników nie korzysta z systemu (np. w porze nocnej). Jeśli więc ktoś jutro nie będzie pracował, będziemy musieli poczekać do końca bieżącego dnia, a następnie odpowiednio zmienić jego role i status. Na przykład, jeśli mamy pracowników, którzy aktualnie pracują i pełnią rolę „pracownik call center”, otrzymają oni listę klientów, do których muszą zadzwonić. Jeśli ktoś przez pomyłkę miał ten status i rolę, zdobędzie również swoich klientów, a my będziemy musieli poświęcić czas na poprawienie tego.
-
Użytkownik może mieć tylko jedną rolę na raz.
Ogólnie użytkownicy powinni mieć więcej niż jedną rolę w systemie. Może w czasie, gdy projektujesz bazę danych, nie ma takiej potrzeby. Pamiętaj, że mogą wystąpić zmiany w przepływie pracy/procesie. Na przykład w pewnym momencie klient może zdecydować się na połączenie dwóch ról w jedną. Jednym z możliwych rozwiązań jest utworzenie nowej roli i przypisanie do niej wszystkich funkcjonalności z poprzednich ról. Innym rozwiązaniem (jeśli użytkownicy mogą mieć więcej niż jedną rolę) jest to, że klient po prostu przypisuje obie role użytkownikom, którzy ich potrzebują. Oczywiście to drugie rozwiązanie jest bardziej praktyczne i daje klientowi możliwość szybszego dostosowania systemu do jego potrzeb (co nie jest obsługiwane przez ten model).
Z drugiej strony ten model ma też jedną dużą przewagę nad innymi. To proste, więc zapytania o zmianę statusów i ról również byłyby proste. Również zapytanie sprawdzające, czy użytkownik ma uprawnienia do logowania się do systemu, jest znacznie prostsze niż w innych przypadkach:
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;
@nazwa_użytkownika i @hasło to zmienne z formularza wejściowego, podczas gdy zapytanie zwraca identyfikator użytkownika i identyfikator roli, który posiada. W przypadkach, gdy nazwa_użytkownika lub hasło są nieprawidłowe, para nazwa_użytkownika i hasło nie istnieje lub użytkownik ma przypisany status, który nie jest aktywny, zapytanie nie zwróci żadnych wyników. W ten sposób możemy zabronić logowania.
Ten model może być używany w przypadkach, gdy:
- jesteśmy pewni, że nie będzie żadnych zmian w procesie, które wymagają od użytkowników posiadania więcej niż jednej roli
- nie musimy śledzić zmian ról/stanu w historii
- nie oczekujemy, że będziemy mieć dużo administracji ról/stanów.
Dodano składnik czasu
Jeśli musimy śledzić rolę i historię statusu użytkownika, musimy dodać wiele do wielu relacji między user_account
i role
i user_account
i status
. Oczywiście usuniemy role_id
i status_id
z user_account
stół. Nowe tabele w modelu to user_has_role
i user_has_status
a wszystkie pola w nich zawarte, z wyjątkiem godzin zakończenia, są obowiązkowe.
Tabela user_has_role
zawiera dane o wszystkich rolach, jakie kiedykolwiek mieli użytkownicy w systemie. Alternatywny klucz to (user_account_id
, role_id
, role_start_time
), ponieważ nie ma sensu przypisywać tej samej roli użytkownikowi więcej niż raz.
Tabela user_has_status
zawiera dane o wszystkich statusach, jakie użytkownicy mieli kiedykolwiek w systemie. Alternatywny klucz to (user_account_id
, status_start_time
), ponieważ użytkownik nie może mieć dwóch statusów, które zaczynają się dokładnie w tym samym czasie.
Czas rozpoczęcia nie może być zerowy, ponieważ wstawiając nową rolę/status znamy moment, od którego rozpocznie się. Czas zakończenia może być pusty, jeśli nie wiemy, kiedy rola/status się skończy (np. rola jest ważna od jutra, aż coś się wydarzy w przyszłości).
Oprócz pełnej historii, możemy teraz dodawać statusy i role w przyszłości. Ale to powoduje komplikacje, ponieważ musimy sprawdzić nakładanie się podczas wstawiania lub aktualizacji.
Na przykład użytkownik może mieć jednocześnie tylko jeden status. Zanim wstawimy nowy status, musimy porównać czas rozpoczęcia i zakończenia nowego statusu ze wszystkimi istniejącymi statusami dla tego użytkownika w bazie danych. Możemy użyć takiego zapytania:
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
i @end_time
to zmienne zawierające czas rozpoczęcia i czas zakończenia statusu, który chcemy wstawić oraz @user_account_id
to identyfikator użytkownika, dla którego go wstawiamy. @end_time
może być null i musimy go obsłużyć w zapytaniu. W tym celu wartości null są testowane za pomocą ifnull()
funkcjonować. Jeśli wartość jest null, przypisywana jest wysoka wartość daty (na tyle wysoka, że gdy ktoś zauważy błąd w zapytaniu, już dawno znikniemy :). Zapytanie sprawdza wszystkie kombinacje czasu rozpoczęcia i zakończenia pod kątem nowego statusu w porównaniu z czasem rozpoczęcia i zakończenia istniejących statusów. Jeśli zapytanie zwróci jakiekolwiek rekordy, oznacza to, że nakładamy się na istniejące statusy i powinniśmy zabronić wstawiania nowego statusu. Byłoby również miło zgłosić błąd niestandardowy.
Jeśli chcemy sprawdzić listę aktualnych ról i statusów (praw użytkownika), po prostu testujemy, używając czasu rozpoczęcia i zakończenia.
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
i @password
są zmiennymi z formularza wejściowego, podczas gdy @time
można ustawić na Now(). Gdy użytkownik próbuje się zalogować, chcemy sprawdzić jego uprawnienia w tym czasie. Wynikiem jest lista wszystkich ról, które użytkownik ma w systemie w przypadku, gdy nazwa_użytkownika i hasło są zgodne, a użytkownik ma aktualnie aktywny status. Jeśli użytkownik ma status aktywny, ale nie ma przypisanych ról, zapytanie niczego nie zwróci.
To zapytanie jest prostsze niż to w sekcji 3, a ten model umożliwia nam posiadanie historii statusów i ról. Ponadto możemy zarządzać statusami i rolami na przyszłość i wszystko będzie działać dobrze.
Model końcowy
To tylko pomysł na to, jak można zmienić poprzedni model, gdybyśmy chcieli poprawić osiągi. Ponieważ użytkownik może mieć tylko jeden aktywny status na raz, możemy dodać status_id
na user_account
tabela (current_status_id
). W ten sposób możemy przetestować wartość tego atrybutu i nie będziemy musieli dołączać do user_has_status
stół. Zmodyfikowane zapytanie wyglądałoby tak:
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
Oczywiście upraszcza to zapytanie i prowadzi do lepszej wydajności, ale istnieje większy problem, który wymaga rozwiązania. current_status_id
w user_account
tabelę należy sprawdzić i w razie potrzeby zmienić w następujących sytuacjach:
- przy każdym wstawieniu/aktualizacji/usunięciu w
user_has_status
stół - każdego dnia w zaplanowanym wydarzeniu powinniśmy sprawdzać, czy czyjś status się zmienił (obecnie aktywny status wygasł lub/i jakiś przyszły status stał się aktywny) i odpowiednio go aktualizować
Rozsądnie byłoby zapisać wartości, których zapytania będą często używać. W ten sposób unikniemy powtarzania tych samych kontroli i dzielenia pracy. Tutaj unikniemy dołączenia do user_has_status
tabeli i wprowadzimy zmiany w current_status_id
tylko wtedy, gdy się wydarzą (wstaw/aktualizuj/usuń) lub gdy system nie jest tak często używany (zaplanowane zdarzenia zwykle są uruchamiane, gdy większość użytkowników nie korzysta z systemu). Może w tym przypadku niewiele zyskalibyśmy na current_status_id
ale spójrz na to jako na pomysł, który może pomóc w podobnych sytuacjach.