Kilka typów użytkowników + relacje między nimi + dziedziczenie?

To mój pierwszy post na tym forum, więc witam wszystkich serdecznie!
Z Railsem (wersja 3) zacząłem swoją przygodę stosunkowo niedawno, trochę rzeczy już wiem ale jeszcze dużo przede mną.

Mam przed sobą następujący problem: muszę stworzyć system, w którym istnieją 3 typy użytkowników: administrator, recepcja, pracownik. Uwierzytelnianie użytkownika odbywa się za pomocą AuthLogica, autoryzacja za pomocą Cancana. Relacje między typami użytkowników wyglądają tak: administrator ma wielu recepcji, recepcja ma wielu pracowników (dodatkową kwestią jest, że recepcja musi mieć dostęp do niektórych obiektów, które są powiązane z pracownikiem, czyli np.

[code]#Reception.rb
has_many :workers
has_many :events
has_many :events, :through => :workers

#Worker.rb
belongs_to :reception
has_many :events

#Event.rb
belongs_to :worker
belongs_to :reception[/code]
jak widzicie recepcja może mieć również swoje własne eventy i sprawa zaczyna się zaciemniać). Dodatkowym utrudnieniem w całej sytuacji jest to, że recepcja musi mieć dodatkowe atrybuty, które nie wchodzą w skład atrybutów administratora i pracownika - co wiążę się z utrudnioną budową formularzy i bazy. Miałem zamiar całość zrobić w taki sposób, żeby Administrator, Reception i Worker, dziedziczyły z klasy User, za pomocą pola type. Pierwszym problemem, który mi się nasunął jest to, że nie mogę wtedy stworzyć dla recepcji oddzielnej tabeli z dodatkowymi atrybutami, które musi posiadać (kiedy spróbowałem to zrobić pola z tabeli reception, dla obiektu reception są niewidoczne undefined method `nazwa_pola’ for #Reception:0x4281048). Cóż mógłbym na siłę dodać te pola do tabeli User i wykorzystywać tylko dla użytkowników typu recepcja (co myślicie na temat takiego rozwiązania?). Drugą kwestią jest to, że czytałem o problemach, które wiążą się z dziedziczeniem i relacjami między obiektami, który dziedziczą tą samą klasę (ktoś mógłby to potwierdzić, czy faktycznie jest lepiej tego nie robić? sam nie miałem jeszcze czasu aby sprawdzić to w praktyce).

Drugim rozwiązaniem, które przychodzi mi do głowy jest stworzenie tabel administrator, reception i worker, z odpowiednimi dla nich polami które są w relacji z tabelą User.

#User.rb has_one :administrator has_one :reception has_one :worker
Jednak te rozwiązanie wydaje się mało sympatyczne z perspektywy kontrolera, w którym musiałyby się mieszać obiekty typów użytkownika z obiektem User.

Mam mały zamęt w głowie, z racji tego, że nigdy nie mierzyłem się z takim problemem, więc proszę wszystkich, którzy mają doświadczenie w tej kwestii, lub po prostu wiedzą jakim schematem postępowania, należy to rozwiązać, o pomoc lub linki do materiałów z takową. Z góry dziękuję za odpowiedzi!

Musisz jeszcze raz siąść i opisać nam dokładnie problem Twojej dziedziny. Niestety poplątałeś już dziedzinę z częściową implementacją i to nie pozwala nam (mi) na zrozumienie problemu. Przykład: co to znaczy, że administrator ma wiele recepcji? (wydaje się nielogiczne). Przedstaw problem, implementację odłóż na potem.

Dzięki za tego posta – na szczęście nie tylko ja nie mogłem zrozumieć o co tu chodzi :slight_smile:

  1. Railsy domyślnie wspierają STI (implementację dziedziczenia na poziomie bazy danych, gdzie wszystkie modele trzymane są w jednej tabeli) - dlatego najbardziej bezproblemowe jest rozwiązanie, gdzie user jest klasą nadrzędną i z niego dziedziczą pozostałe klasy.
  2. W zasadzie nie trzeba się bardzo przejmować tym, że jest więcej pól w bazie dla danego modelu. Korzystając z “attr_accessible” możesz ograniczyć modyfikowalność tych pól, które nie należą do określonej klasy pochodnej (co nie znaczy, że model nie będzie ich posiadał - nie będę np. zmieniane przy masowym przypisaniu, tzn. update_attributes).
  3. Skoro recepcja ma mieć swoje eventy, to nie może mieć również eventów pracownika. Masz do nich zawsze dostęp pośredni i możesz sam sobie dopisać odpowiednią metodę typu “worker_events”.
  4. Piszesz event belongs_to user, zamiast worker - możesz dodać dodatkową walidację, jeśli nie chcesz, żeby administrator mógł mieć eventów.

Ad. 1 dotychczas byłem zwolennikiem takiego rozwiązania, ale ostatnio Paneq zaproponował rozwiązanie (choć w trochę innym kontekście), które wydaje mi się lepsze. Tzn - w Rubim możesz wspólną implementację (dla pracownika, administratora, etc.) wrzucić do modułu i ten moduł “includować” do odpowiednich klas. Ma to swoje zalety, choć z drugiej strony na początku prościej będzie pójść drogą, którą wytyczają Railsy (tzn. STI).

@radarek, @hubert widać macie małe doświadczenie dydaktyczne :wink:

Moje doświadczenie wskazuje, że STI to zło w sytuacji kiedy istnieje możliwość (a co za tym idzie czasami i potrzeba) zmiany typu w jakąś stronę. Przykładowo Administrator to jest rola jakiejś osoby a nie coś co powinno dziedziczyć z User.

Dlaczego ?

Ponieważ w Railsach nie istnieje trywialne obejście problemu w którym chcemy wziąć utworzony wcześniej obiekt klasy Administrator i zapisać go z powrotem jako User. Jeśli to zrobimy nadpisując pole type (domyślnie używane do rozpoznawania klasy STI) to walidacje nie wykonają się i tak na klasie Administrator. Można to wszystko nawet dość prosto obejść jeśli się zajrzy w kod Rails, sam mam w projekcie jeden przypadek dla którego ma to sens ale zwykle fakt, że takie sytuacje mogą wystąpić oznacza, że nie powinniśmy projektować aplikacji w oparciu o STI.

Totalnie najbardziej beznadziejnym przypadkiem jaki często się w książkach przedstawia jest ten nieszczęśny Manager, który odróżnieniu od Pracownika ma inny sposób liczenia pensji i metoda do tego jest nadpisana. Na pohybel ze słabymi przykładami które uczą programistów złych praktyk.

Reasumując: Pomyśl 2 razy nad użyciem STI pytając się czy typ czegoś może się zmienić w toku życia aplikacji. Jeśli ktoś może przestać być administratorem i znów być tylko userem to znaczy, że to jest jego rola którą można mu dodawać i zabierać a nie jego immanentna właściwość

A jeszcze się wypowiem trochę: Dziedziczenie ogólnie jest często spoko przy technicznych elementach aplikacji w stylu:

a) button z obrazkiem dziedziczy ze zwykłego buttona, który zaś dziedziczy z kontroler gui, które zaś dziedziczą z czegoś jeszcze itp itd…
b) xhtml parser dziedziczy z XmlParser a on zaś z klasy Parser itp.

Tam to często dobrze działa i jest ok gdyż odpowiedzialności takich komponentów się nie zmieniają. Guzik jest zawsze guzikiem a okienko programu nie ma w czasie jego działania stać się niczym więcej jak okienkiem programu. Natomiast osoby/użytkownicy często czynią wiele rzeczy w programie i ich odpowiedzialności oraz role się zmieniają (w sensie tego, że z czasem mogą być różne bo zmienia się struktura w firmie oraz w sensie takim, że różne moduły aplikacji różnych rzeczy oczekują od użytkownika).

@paneq - zwróciłeś uwagę na istotny problem, zastanawiam się tylko, czy ten scenariusz o którym mówimy w kontekście STI jest faktycznie typowy, tzn. że jakiś obiekt zmienia swoje role i odpowiedzialności. Niewątpliwie w przypadku użytkownika tak faktycznie może się dziać (tzn. jeśli założymy, że jego rola ma być wyznaczana przez klasę, a wcale tak być nie musi). Ale w przypadku powiedzmy produktów - które dzielą się na książki, płyty CD i powiedzmy DVD, to raczej nie będziemy mieli do czynienia z taką “zmianą substancjalną”. Śmiem twierdzić, że jednak taka sytuacji jest bardziej typowa i wtedy STI nie posiada problemu, o którym piszesz.

A z drugiej strony - innym możliwym rozwiązaniem problemu (niż zamiana pola Type) przy zmianie roli/odpowiedzialności, jest stworzenie kopii danego użytkownika w nowym modelu i przeniesienie danych (tych które mają sens) do tej kopii, a następnie usunięcie oryginału. W tym wypadku walidacje zadziałają jak należy - a jeśli coś pójdzie nie tak, to po prostu wycofa się transakcję.

Wszystko to nie zmienia faktu, że połączenie reprezentacji zewnętrznego aktora (vide modelu user) z modelem danych (np. określenie związków z innymi modelami) jest zawsze nieco problematyczne.

Do tego właśnie mam metodę ale nie było to wszystko takie łatwe. Ostatecznie skończyło się na własnej metodzie, która pod spodem używa http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-becomes , dodatkowo zmieania wartość pola “type” oraz jednym hacku. STI domyślnie próbuje zrobić update wstawiając w warunki zapytania sql nie tylko ID rekordu ale także nazwę tej klasy na której próbuje zrobić update więc żeby to działało trzeba było usunąć to niepożądane zachowanie bym mógł właśnie ten typ zmienić.

Tak, faktycznie tego typu rzeczy raczej się nie zmieniają w coś innego :slight_smile: Na szczęście żyjemy w takim świecie :-). Nie wiem czy jest to sytuacja bardziej typowa ale na pewno lepsza by pokazać kiedy dziedziczenie ma sens a kiedy niekoniecznie.

Swoją drogą jakiś czas temu czytałem świetny artykuł który fajnie próbował bawić się innym mega znanym przykładem czyli Kwadrat dziedziczy z Prostokąta. I co ciekawe o ile w rozumieniu matematycznym kwadrat jest prostokątem czyli mamy spełniony klasyczny warunek który podaje się w książkach: “is a” to z punktu widzenia programistycznego sprawa może się komplikować. Jeśli bowiem zrobimy prostokątom metodę setSize(x, y) i możemy ją wywoływać z jakimiś parametrami to w kodzie który działa na prostokątach nie możemy podstawić kwadratu zamiast prostokąta bo wywołanie to może się nie powieść jeśli x będzie różne od y.

Uważam, że to może jednak czasem być słaby pomysł aby robić tabelę typu STI na produkty. Wydaje mi się, że mają one tyle różnych cech, że nia ma sensu tego pakować do jednej tabeli. Te trzy rzeczy które wymieniłeś mają coś tam wspólnego ze sobą, że wydawać by się to mogło sensowne. Ale jeśli za pół roku ten sam sklep zacznie sprzedawać waciki (i będziesz musiał zapisać ilość wacików) oraz filmy w postaci plików do pobrania to nagle jesteśmy w kropce. Bardziej bym tutaj szedł w stronę rodzajów produktów dla których można by definiować pewne właściwości które one posiadają i które można lub trzeba dla nich podać. Wtedy pewnie stwierdziłbym, że to może jest bardzo dobry moment, żeby przeczytać coś o bazach nosql, które chyba łatwiej by sobie z czymś takim poradziły ale nigdy z nich nie korzystałem i mówię to zdanie bardziej w oparciu o moje wyobrażenie o nich niż o rzeczywiste doświadczenie.