Wpadłem w pułapkę relacji wiele do wielu, m:n. Po raz kolejny. Jak fretka w sidła… te same po raz n-ty (czy m-ty?). Byłem na siebie tak wściekły, że to ludzkie pojęcie przechodzi. Właściwie bardziej mi teraz szkoda nerwów niż straconego czasu.
Ale o co się, panie, rozchodzi?
W moim systemie miałem bardzo prostą zależność: Company (n) – (m) Customer. Firma posiada wielu klientów, a klient przypisany jest do wielu firm. Bez dodatkowych ceregieli utworzyłem:
1: public class Company 2: { 3: public virtual ICollection<Customer> Customers { get; set; } 4: // ... 5: } 6: 7: public class Customer 8: { 9: public virtual ICollection<Company> Companies { get; set; } 10: // ... 11: }
WIEDZIAŁEM że coś jest nie tak. Już dawno, dawno temu obiecałem sobie, że nigdy nie dopuszczę do takiej sytuacji. Sytuacji, którą nazwałbym "implicit/direct many to many relation" – może niekoniecznie poprawnie. Pozostawienie takiej zależności jest proszeniem się o kłopoty. Samoograniczenie do potęgi N.
Mimo tego, że zdawałem sobie sprawę z wad powyższego rozwiązania, nie uporałem się z nim natychmiast. Zamiast tego pomyślałem "e tam, zobaczymy jak to się rozwinie, może będzie dobrze". Pomyślałem tak sobie wiele razy, co kilka dni.
Gdybym zaradził temu od razu, poświęciłbym całemu zadaniu wprowadzenia dodatkowej klasy reprezentującej relację pomiędzy klientem a firmą raptem godzinkę, może dwie. Jednak wszystko strzeliło dopiero dwa miesiące później, gdy pojawiło się wymaganie:
"Chciałbym wiedzieć kiedy klient został skojarzony z firmą; dodatkowo klient może mieć pewne preferencje dotyczące firm, każdej z osobna; przydałaby się również informacja kto klienta do firmy przypisał"
Dodanie klasy:
1: public class CompanyCustomerAssociation 2: { 3: public virtual Company Company { get; set; } 4: public virtual Customer Customer { get; set; } 5: public virtual DateTime CreationDate { get; set; } 6: // ... 7: }
nie było już wtedy tak banalne. Gdy po dobrej godzinie czy półtorej udało mi się skompilować projekt – posypało się prawie 150 testów. Poprawienie całego kodu zajęło mi kolejne 6 czy 7 godzin. Ręczne testy (tak dla całkowitej pewności…) – kolejne 2 godziny.
A to wszystko przez… no właśnie, nie wiem przez co. Chyba pozostaje jedynie określenie GŁUPOTA. Od samego początku wiedziałem, że robię źle. Od samego początku wiedziałem, że wcześniej już się nadziałem na ten bolec. Ignorując zdobyte w równie bolesny sposób doświadczenie postąpiłem tak, jakbym wcześniej nie był w takiej sytuacji i się przy tej okazji nie sparzył.
Ale dosyć tego, to był ostatni raz. Od tego dnia relacje m:n dla mnie nie istnieją. ZAWSZE będę jawnie tworzył obiekt łączący encje w ten sposób. Może się przyda, a może nie… w bazie danych i tak musi występować dodatkowa tabela.
Another lesson re-learned,
Procent, the Moron King
True ;)
O takich i podobnych pułapkach można poczytać w ciekawym artykule (jeden z wielu) pod tym adresem:
http://www.redpill.com.pl/artykuly/akademia/114-role
@Grzessiekk:
Dzięki za linka, wygląda cool, zostawiam na później:).
A propos pułapek…
Załóżmy, że masz formularz, na którym korzystasz z klasy Company.
Jak się chronisz przed sytuacją, gdy zmienia się np. adres firmy ?.
Na formularzach z przeszłości powinna być zachowany stary adres a na nowo tworzonych formularzach aktualne dane.
Przy zastosowaniu bazy relacyjnej domyślnie. niestety zmienia się wszędzie nazwa firmy, także na formularza w przeszłości.
Pozdrawiam
Grzessiekk
Witam,
Czytam z zaciekawieniem Twojego bloga od jakiegoś czasu, ale pierwszy raz komentuję.
Spotykam się z podobnym problemem przy prawie każdym projekcie. Relację n:m zawsze modeluję jako osobną klasę. Czasem nawet zwykłą relację 1:n modeluję jako osobną klasę. Nawet jeśli z wymagań to nie wynika wolę na zapas się zabezpieczyć. Może jest trochę więcej pracy, ale w razie zmiany wymagań niewiele trzeba zmieniać.
Do Grzessiekk:
Jeśli ktoś w połowie projektu powie mi, że chciałby aby dane teleadresowe były jednak archiwizowane, to go przeklnę i rzucę fatwę, a następnie dorobię tabelę w relacji 1:n z adresami z odpowiednią kolumną informującą o dacie ważności poszczególnych wpisów. Wtedy pobranie danych do starych formularzy następuje poprzez filtrowanie danych z odpowiednią datą.
Pozdrawiam
Bartek
@Grzessiekk:
Jeżeli sytuacja wymaga, aby dane historyczne nie zostały nigdy zmienione, to mam dwa wyjścia:
1) kopiuję lokalnie wszystkie dane
Na przykład zamówienie odnosi się do produktów, których cena może się zmienić – wtedy w każdym zamówieniu przechowuję po prostu aktualną cenę na dzień złożenia zamówienia.
2) zabraniam edycji danych
To miałoby miejsce we wspomnianej przez Ciebie sytuacji. Adres to adres i jako taki nie może się zmienić sam w sobie. Firma może dostać nowy adres, przez co aktualnie jest widoczna pod nową lokalizacją, ale dany formularz ciągle ma referencję do starego adresu, którego dane pozostają takie same na wieki wieków ameno.
@barozi:
Zatem witam kolejnego komentatora i zapraszam do dalszego udzielania się:)
Z przedstawianiem n:1 w postaci osobnej klasy chyba się nie spotkałem i nie stosowałem tego, muszę nad tym więcej pomyśleć:).
Dość fajnie cały problem relacji opisał Eric Evan w książce o DDD. To jak dane sobie siedzą w samej bazie nie powinno mieć wpływu na nasz model domeny. Na przykład przy relacji 1:n – jak chociażby podana przez Grześka sytuacja – w bazie ID rodzica jest przechowywane w tabeli dzieci. Ale już w modelu obiektowym nie ma wcale konieczności aby Adres miał referencję do swojej Firmy… bo i po co. Wystarczy kolekcja Adresów na Firmie.
Takie redukowanie wzajemnych referencji między klasami może bardzo uprościć model i staram się każdą relację badać pod tym kątem – czy na pewno nie mogę jej wywalić ze swojej obiektowej wizji domeny?
To jak w takim razie rozwiązać ten problem i postępować właściwie?
Jakiś fragment kodu byłby mile widziany :)
@Krzysiek:
Po pierwsze: nie chcę używać słowa "właściwie/niewłaściwie". To kwestia względna i być może ktoś inny radzi sobie z tym w inny sposób, a mój zjedzie od góry do dołu (chociaż tego bym nie zrozumiał:) ).
A po drugie: kod jest przecież wyżej, w tym przypadku rozwiązaniem była klasa pośrednicząca CompanyCustomerAssociation. Ma referencje do Firmy i do Klienta, a Firma i Klient mają kolekcje jej instancji. Dzięki temu zamiast n:m mamy n:1 i 1:m.
Patrząc od strony domain driven design, relacje implicite są złem wcielonym. Ten kawałek kodu z początku posta nie mówi zupełnie nic na temat roli tych kolekcji. Podejście z obiektem mapowanym do tabeli łączącej jest dużo lepsze, ponieważ pozwala wyrazić intencję. Dlatego właśnie zamiast nazywać tę encję łączącą CompanyCustomerAssociation (co jest terminem czysto technicznym), nadałbym jej jakąś nazwę związaną z domeną, np. CustomerRegistration, CompanyRegistration, CustomerContact lub coś w tym stylu.
@Szymon Pobiega:
Nazwa też mi się nie do końca podoba i zdecydowałem się na nią po dobrych kilku minutach dumania. Ale akurat w tym przypadku faktycznie ma ono przełożenie na domenę – jest to powiązanie między klientem a firmą i tak samo określiłbym to, gdyby rozmowa była całkowicie oderwana od kodu. Ale zgadzam się że trzeba na to uważać.