Active Record a dziedziczenie modeli

Zalozmy ze mamy taka sytuacje:

[code]class User < ActiveRecord:Base
# relacje
# _relacje specjalne
end
class Employee < User
#validacja tylko dla employee
#def relacja

return(_relacja_specjalna)

#end
end
class Contact < User
#validacja tylko dla employee
#def relacja

return(_relacja_specjalna)

#end
end[/code]
Jak widac to nic skomplikowanego, 1 obiekt User ktory posiada relacje i relacje specjalne, 2 obiekty dziedzicza po obiekcie User oraz wykorzystuja jego relacje specjalne, ale oprocz tego posiadaja indywidualna walidacje. Wykorzystanie takiego modelu danych jest bardzo przyjemne w rails, posiadamy jedna tabele, tworzymy w przejrzysty sposob okreslone relacje, ograniczami bardzo mocno ilosc kodu, wiecej jest relacji wspoldzielonych niz tych specjalnych, jednak wydaje mi sie ze w AR istnieje bardzo powazna wada takiego modelu otoz.

Otoz jak przekrztalcic istniejacy juz w bazie obiekt Contact na Employee i odwrotnie wykonujac przy tym validacje ?

oczywiscie validacja na poziomie User nie wchodzi w gre.

Nie wiem jak inni ale ja nie rozumiem :).

Napisalem troche zawile ale coz, zmeczenie.

Problem polega na tym ze po stworzeniu obiektu Employee.new i po wrzuceniu go do bazy Employee.save nie jestem w stanie przekrztalcic go w obiekt Contact, oba te obiekty korzystaja z tego samego zestawu danych (User, tabela users).

Pobierajac obiekt w postaci User.find(id) dostaje zawsze konkretny typ (Employee|Contact) a wiec jest to kolejny problem. Nastepny to to ze Employee.update_attribute(:type,‘Contact’) spowoduje zmiane typu, w tabli kolumna Type zostanie zmieniona, natomiast Employee.update_attributes(:type => ‘Contact’) nie zmieni nic. To samo sie bedzie tyczyc @employee.type = ‘Contact’.

Jedyny sposob zrzutowania Contact na Employee i na odwrot jak narazie to zapamietanie typu, jego zmiana, zvalidowanie formularza i w przypadku bledu przywrocenie typu.

Czy moje pojecie obiektowosci jest bledne, czy po prostu AR ma nieprzemyslane dziedziczenie modeli.

Wydaje mi się, że mogłeś popełnić jakiś błąd projektowy. Single Table Inheritance daje proste odwzorowania dziedziczenia klas. Równie dobrze możesz powiedzieć, że Ruby jest głupie bo nie można w nim zmienić typu (klasy) zmiennej.

Twoje pojęcie obiektowości jest jak najbardziej prawidłowe(jeżeli chodzi o koncepcje dziedziczenia z klasy user). Jednak proponuje zapamiętać, że:

Relacyjne bazy danych + dziedziczenie = problemy

Po prostu odwzorowywanie dziedziczenia w relacyjnej bazie jest procesem topornym. Według mnie należy go unikać kiedy to jest możliwe(podobną opinię znajdziesz też w książce Agile Development with Rails).

Wydaje mi się, że lepszym sposobem modelowania opisanego przez Ciebie problemu jest użycie mechanizmu opartego na rolach. Wtedy dane dla użytkownika zostają zawsze takie same i ewentualnie dodajesz dane dla danej roli(i oczywiście przypisujesz daną rolę użytkownikowi).

To ze nie mozna zmienic typu klasy to akurat jasna sprawa, ale chodzi mi tu o cos innego, o to ze nie moge w prosty sposob przekrztalcic tego obiektu w podobny, bazujacy na tej samej klasie pierwotnej. A jeszcze bardziej wkurza mnie to ze nie moge otrzymac wlasnie obiektu pierwotnego.

User.find(employee_id) -> Employee
Employee.find(employee_id) -> Employee
Contact.find(employee_id) -> nil

gdyby User.find(employee_id) -> User to nie bylo by problemu, dlaczego ? bo moglbym napisac cos takiego:

[code]class User < ActiveRecord:Base
validate_presence_of :password, :if => :employee?

def employee?
@attributes[:type] == ‘Employee’
end[/code]
Wtedy problem bylby rozwiazany. To nie jest moj blad projektowy, ja musze miec taka klase i takie 2 obiekty dziedziczace ja, oraz musze miec mozliwosc przechodzenia jednego obiektu w drugi, zaoszczedzam w ten sposob naprawde mnostwo pracy. No chyba ze mowisz o bledzie projektowym w kontekscie rails, ze w rails sie po prostu tak nie da, to owszem, wyglada na to ze sie nie da.

System rol juz jest wykorzystywany i akurat problem polega na tym ze role posiadaja wlasnie tylko uzyytkownicy typu Employee.

Pozatym ten system jest naprawde swietny jesli chodzi o ograniczenie ilosci kodu. Wyglada na to ze bede musial po prostu przeniesc wszystko z Contact i Employee do User, utworzyc nowa kolumne, np is_employee i wszystkie relacje oraz validacje specjalne dla employee uwarunkowac wlasnie na podstawie tej kolumny. Naszczescie nie jest tego az tak duzo. Natomiast znacznie gorzej jest z aplikacja.

zamiast
paginate :employees, bede musial pisac paginate :users, :conditions => [‘is_employee = 1’] i chyba na tym sie nie skonczy :frowning:

[quote=PaK]To ze nie mozna zmienic typu klasy to akurat jasna sprawa, ale chodzi mi tu o cos innego, o to ze nie moge w prosty sposob przekrztalcic tego obiektu w podobny, bazujacy na tej samej klasie pierwotnej. A jeszcze bardziej wkurza mnie to ze nie moge otrzymac wlasnie obiektu pierwotnego.

User.find(employee_id) -> Employee
Employee.find(employee_id) -> Employee
Contact.find(employee_id) -> nil

gdyby User.find(employee_id) -> User to nie bylo by problemu[/quote]
Wtedy zupełnie rozminąłbyś się z ideą dziedziczenia. Ponieważ User jest klasą bazową, to pisząc User.find(…) chcesz znaleźć wszystkich użytkowników (a więc Employee i Contact) o określonych warunkach. I co najlepsze dostajesz tablicę, w której mogą być obiekty zarówno jednej jak i drugiej klasy. Ale jak byś rozpoznawałbyś co to konkretnie za obiekty jeśli wszystkie byłyby klasy User? (no dobra masz pole ‘type’, ale po co Ci wtedy całe dziedziczenie, nadpisywanie metod z klasy bazowej itp).

[quote=PaK], dlaczego ? bo moglbym napisac cos takiego:

[code]class User < ActiveRecord:Base
validate_presence_of :password, :if => :employee?

def employee?
@attributes[:type] == ‘Employee’
end[/code]
Wtedy problem bylby rozwiazany.[/quote]
To już jest kompletnie bez sensu. W klasie bazowej zakładasz, że istnieje klasa dziedzicząca ‘Employee’, co nie jest zbyt ładne. W takim wypadku chyba lepiej zrezygnować z dziedziczenia. A poza tym nie widzę problemu żeby tą specjalną walidację przerzucić do klasy Employee:

[code]class User < ActiveRecord::Base
#wspólna walidacja
end

class Employee < ActiveRecord::Base
#walidacja specyficzna dla employee
validate_presence_of :password
end[/code]

No niestety miałem na myśli Twój błąd projektowy :). Tak jak piachoo pisał, dziedziczenie mapowane do relacyjnej bazy czasem może być problematyczne i najlepiej trzymać się pewnych schematów. Ja wychodzę z prostego założenia w tym wypadku. Jeśli dziedziczę, to dlatego, że mam pewną wspólną klasę bazowę, z której wywodzą się inne specjalistyczne, mające (to najważniejsze) inne zachowanie (inaczej robią coś, walidują się itp).

Albo ja naprawde pisze jak potluczony albo nie doczytales, mi wlasnie chodzi po pozbycie sie dziedzicznosci i podzial mechanizmow wewnatrz klasy na dwa typy, dla Kontaktu i dla Pracownika. Jesli zrobie tak jak napisalem wyzej to to juz nie bedzie klasa bazowa.

Po co ? chodzby po to aby moc przekrztalcic User (Employee) -> User (Contact). Gdy dostaje zawsze, ZAWSZE typ z najwyzszego poziomu w hierarchi, nawet wtedy gdy odwoluje sie do poziomu nizej (wlasnie poprzez User.find()) nie mam zadnych mozliwosci wplywu na to czym ten obiekt tak naprawde jest.

Take same byly i moje zalozenia, przeciez na poczatku to podkreslilem, osobna walidacja, osobne relacje specjalne, wykorzystanie wspolnych relacji z klasy bazowej. To czego mi brakuje to zrzutowanie obiektu na pokrewny w hierarchi, w rails moge jedynie zrobic Contact.update_attribute(:type, ‘Employee’), ale nie jestem w stanie tuz po takiej akcji wykonac jakichs ruchow zwiazanych wlasnie z Employee, np zvaidowac czy ten uzytkownik faktycznie moze byc zapisany w bazie jako Employee (a moze tylko wtedy gdy dane z formularza sa poprawne).

To co jedynie teraz moge zrobic to po prostu akcje Change Type ktora zmienia typ uzytkownika i ustawia go w “nieaktywnego”, uzytkownik bedzie musial wejsc do systemu i go aktywowac, a wtedy dopiero bedzie faktycznie wykonana walidacja danych.

W javie tez nie mozna bezposrednio zrzutowac na siblinga.

Tak jak wspominal piachoo, rozwiazanie bazujace na rolach jest najb. sensowne w Twojej sytuacji. Ja dołożylbym i atrybut ‘kind_of’ albo ‘classification’ i mial problem z głowy.

Na upartego mozna by napisac jakas metode np.

[code]class User < ActiveRecord::Base

def to_sibling(clazz)
# sprawdz czy clazz jest na tym samym drzewie (jak nie to wyjatek)
self[:type] = clazz.to_s
save(false) # pomin walidacje z obecnego obiektu
clazz.find(id)
end
end

i w kontrolerze

user = Employee.find(123)
user.to_sibling(Contact)
user.save # false jesli walidacja sie nie udala ale juz jako Contact[/code]

Albo ja naprawde pisze jak potluczony albo nie doczytales, mi wlasnie chodzi po pozbycie sie dziedzicznosci i podzial mechanizmow wewnatrz klasy na dwa typy, dla Kontaktu i dla Pracownika. Jesli zrobie tak jak napisalem wyzej to to juz nie bedzie klasa bazowa.[/quote]
No to skoro chcesz się pozbyć dziedziczenia i pozostać przy 1 klasie User to niestety będziesz musiał wszędzie bazować na polu ‘type’ i odpowiednio na jego wartość reagować (inna walidacja, inne zachowanie). Z tym, że jeśli użyjesz warunku

if self.type == 'cos' #... elsif self.type == 'cos innego' #... else #... end
więcej niż raz to będzie jednak sugerować że przydałoby się dziedziczenie… :).

Po co ? chodzby po to aby moc przekrztalcic User (Employee) -> User (Contact). Gdy dostaje zawsze, ZAWSZE typ z najwyzszego poziomu w hierarchi, nawet wtedy gdy odwoluje sie do poziomu nizej (wlasnie poprzez User.find()) nie mam zadnych mozliwosci wplywu na to czym ten obiekt tak naprawde jest.[/quote]
Jeśli coś jest Jabłkiem (class Jablko < Owoc, a więc przy okazji Jalbko jest Owocem) to nim jest i tyle. Jakie ty chcesz mieć nad tym panowanie? Chciałbyś żeby Owoc.find(5) zwróciło obiekt Owoc, a Jalbko.find(5) zwróciło obiekt Jablko? To właśnie wtedy byłby mętlik bo w zależności od tego jak znalazłeś dany obiekt, mógłby się on zachowywać inaczej (inne metody w klasie Owoc i Jablko).

Take same byly i moje zalozenia, przeciez na poczatku to podkreslilem, osobna walidacja, osobne relacje specjalne, wykorzystanie wspolnych relacji z klasy bazowej. To czego mi brakuje to zrzutowanie obiektu na pokrewny w hierarchi, w rails moge jedynie zrobic Contact.update_attribute(:type, ‘Employee’), ale nie jestem w stanie tuz po takiej akcji wykonac jakichs ruchow zwiazanych wlasnie z Employee, np zvaidowac czy ten uzytkownik faktycznie moze byc zapisany w bazie jako Employee (a moze tylko wtedy gdy dane z formularza sa poprawne).[/quote]
Po pierwsze w Rubym nie ma czegoś takiego jak rzutowanie. Rzutowanie, zwłaszcza to w górę, przydaje się w językach statycznie typowanych, gdzie jakieś obiekty chcesz traktować na poziomie wspólnej klasy bazowej (np. lista owoców tj jabłek, gruszek), ale nie po to, żeby nagle zaczęły się inaczej zachowywać (w c++ już jest inna bajka, bo możesz rzutować poprzez wskaźnik, albo poprzez referencję i można się czasem nieźle wkopać :]).

Jeśli myślisz o zmianie typu (klasy) obiektu to musisz iść w kierunku wyciągnięcia z klasy tego co chcesz zmienić i zrobienie z tego relacji ‘posiada’ (kompozycja) niż tak jak masz do tej pory ‘jest’ (dziedziczenie). Zdaje się jest to wzorze projektowy kompozycji (composite pattern). Tym bardziej, że kompozycja jest bardziej elastyczna. Tak jak podpowiadał Ci piachoo łatwiej Ci będzie pójść w kierunku ról (role łatwo wymienić, typ obiektu jak widać nie bardzo :)).

Nie wdając się w dysputy, popieram Radarka i piachoo :wink: