Simple REST web service

0

Cześć, jestem młodym studentem IT programującym głównie w Javie.

Napisałem prostą usługę typu REST z wykorzystaniem Spring Boot'a oraz architektury DDD. Projektem jest system zarządzania biblioteką pozwalający na przeglądanie, rezerwację i wypożyczanie książek.

Byłbym wdzięczny o ocenę projektu i stylu kodowania przez bardziej doświadczonych programistów.

Całe repozytorium projektu: library-backend
Bliźniaczy temat na stackexchange: Simple REST web service

2

@eziomou:
No i tu pojawia się problem pt DDD + JPA. Powiem szczerze, na samym DDD średnio się znam ale występuje problem mieszania logiki biznesowej z frameworkiem i do tego tak "cięzkim" jak JPA. Dodatkowo taki styl narzuca mutowalność, a mutowalność w logice biznesowej na ogół jest zła. Rozumiem mutowalny stan np. socketa, okienka w Swingu (choć akurat w Javie GUI się już za bardzo nie pisze) ale moim zdaniem od czegoś takiego jak


book.reserve() {
     book.reserveDate = LocalDateTime.now(clock);
}


lepsze jest


book.reserve() {
  return book.withReserveDate(LocalDateTime.now(clock))
}

Obiekty które reprezentuję domenę/logikę biznesową powinny być niemutowalne, i własnie dlatego nie lubie klasycznego podejścia do DDD. Wydaje mi się że rozwiązaniem moich problemów jest architektura hexagonalna :D

0
scibi92 napisał(a):

@eziomou:
No i tu pojawia się problem pt DDD + JPA. Powiem szczerze, na samym DDD średnio się znam ale występuje problem mieszania logiki biznesowej z frameworkiem i do tego tak "cięzkim" jak JPA. Dodatkowo taki styl narzuca mutowalność, a mutowalność w logice biznesowej na ogół jest zła. Rozumiem mutowalny stan np. socketa, okienka w Swingu (choć akurat w Javie GUI się już za bardzo nie pisze) ale moim zdaniem od czegoś takiego jak


book.reserve() {
     book.reserveDate = LocalDateTime.now(clock);
}


lepsze jest


book.reserve() {
  return book.withReserveDate(LocalDateTime.now(clock))
}

Obiekty które reprezentuję domenę/logikę biznesową powinny być niemutowalne, i własnie dlatego nie lubie klasycznego podejścia do DDD. Wydaje mi się że rozwiązaniem moich problemów jest architektura hexagonalna :D

Dziękuje za wypowiedź i przejrzenie kodu.

Mógłbyś dokładnie napisać w którym miejscu w kodzie występuje mieszanie logiki biznesowej z frameworkiem oraz szersze uzasadnienie reguły, że obiekty logiki biznesowej powinny być niezmienne?

Książka zaprojektowana jest w ten sposób, dlatego że reprezentuje fizyczną kopie książki występującej w magazynie, jej tożsamość ma znaczenie. Dwie książki o dokładnie tych samych atrybutach ciągle będą innymi książkami, a o ich tożsamości decyduje jedynie identyfikator (w moim przypadku id jest identifykatorem biznesowym). Podobnie jak dwie osoby o tych samych imionach i nazwiskach są ciągle innymi osobami, mają swoją tożsamość oraz ciągłość w czasie.

Gdyby książka była obiektem niezmiennym, w danym momencie działania programu mógłbym mieć dowolną ilość kopii tej samej książki (pod względem tożsamości) o zupełnie innych stanach. Tutaj mógłby wystąpić problem ze stwierdzeniem, która z kopii posiada aktualny stan.

W podejściu DDD stosuje się obiekty niezmienne tzw. Value Object dla obiektów domeny, których tożsamość nie ma znaczenia. Jako przykład mogą posłużyć pięniądze, w większości przypadków nie ma znaczenia tożsamość dwóch obiektów pieniędzy o tej samej wartości, tutaj jak najbardziej mają zastosowanie obiekty niezmienne.

Dodatkowo, wyobraź sobie, że rzeczywiście wszystkie obiekty domeny są niezmienne i akurat tak się zdarzyło, że mamy kilka bardzo ciężkich obietów tzn. takich które mają wiele asocjacji i na każdym z tych obiektów wykonujemy bardzo dużo operacji. W takim przypadku przy każdej operacji tworzona jest głęboka kopia całego drzewa obiektów i być może przez pomyłkę przechowywana gdzieś w pamięci. Byłbym wdzięczny gdbyś uzasadnił stwierdznie, że zmienność obiektów logiki biznesowej jest zła.

1

Mógłbyś dokładnie napisać w którym miejscu w kodzie występuje mieszanie logiki biznesowej z frameworkiem

Obiekty domenowe są zarządzane przez JPA więc występuje wymieszanie logiki z frameworkiem

Tutaj mógłby wystąpić problem ze stwierdzeniem, która z kopii posiada aktualny stan.

A czym to sie różni od używania mutowalnych obiektów?
Aktualny stan ma ten "najnowszy obiekt", jeżeli masz:

val order = orderRepository.findById(orderId)
val sentOrder = order.withSentDate(date)

to sentOrder jest aktualne

W takim przypadku przy każdej operacji tworzona jest głęboka kopia całego drzewa obiektów i być może przez pomyłkę przechowywana gdzieś w pamięci.

Nieprawda. W przypadku gdy mamy niezmienne obiekty możemy łatwo współdzielić różne obiekty między sobą. Jeśli mamy obiekt order który ma liste itemów, to gdy tworzymy nowy order który ma tylko inny status nie musimy w ogóle kopiować "głęboko" tej listy, kopiujemy tylko referencje - zakładając że mamy niemutowalne kolekcje jak Vavr czy ArrowKt.

Byłbym wdzięczny gdbyś uzasadnił stwierdznie, że zmienność obiektów logiki biznesowej jest zła.

Napisałem o tym tutaj:
https://artegence.com/blog/benefits-of-immutability-in-software-development/
Teraz bym dodał element o lokalnym wnioskowaniu - nie musisz wszędzie sprawdzac stanu obiektu przy debugowaniu :)
Pozdrawiam

0

Obiekty domenowe są zarządzane przez JPA więc występuje wymieszanie logiki z frameworkiem

Fakt, że JPA zarządza obiektami domeny nie ma wpływy na logikę biznesową. Na poziomie kodu obiekty domeny mają świadomość tylko o adnotacjach. Podczas pisania warstwy domeny jedynym wymogiem stawianym przez JPA na który zwracałem uwagę był bezargumentowy konstruktor.

A czym to sie różni od używania mutowalnych obiektów?
Aktualny stan ma ten "najnowszy obiekt", jeżeli masz:

val order = orderRepository.findById(orderId)
val sentOrder = order.withSentDate(date)

Kiedy mutawalny obiekt jest już stworzony pracujemy na jednej instancji, jasne w takim prostym przykładzie tego nie widać, chodziło mi o bardziej złożoną sytuację gdzie np. instancja jest przekazywana wiele razy do różnych metod (wymyślam).

Nieprawda. W przypadku gdy mamy niezmienne obiekty możemy łatwo współdzielić różne obiekty między sobą. Jeśli mamy obiekt order który ma liste itemów, to gdy tworzymy nowy order który ma tylko inny status nie musimy w ogóle kopiować "głęboko" tej listy, kopiujemy tylko referencje - zakładając że mamy niemutowalne kolekcje jak Vavr czy ArrowKt.

Jasne, pod warunkiem że mamy niezmienne kolekcje.

Przeczytałem artykuł, rozumiem, że u siebie szeroko stosujesz obiekty niezmienne i masz tutaj doświadczenie w większych projektach. Czy kopiowanie złożonych obiektów po każdej operacji we wszystkich klasach nie zaciemnia logiki, mam na myśli stosunek ilości kodu poświęconego na kopiowanie do ilości kodu właściwej logiki?

1

Fakt, że JPA zarządza obiektami domeny nie ma wpływy na logikę biznesową. Na poziomie kodu obiekty domeny mają świadomość tylko o adnotacjach. Podczas pisania warstwy domeny jedynym wymogiem stawianym przez JPA na który zwracałem uwagę był bezargumentowy konstruktor.

Oczywiście że ma, pomiając już ten bezargumentowy konstruktor (bardzo słabe) to jeszcze jest cykl życia encji, różne jakieś dziwne stany, proxy JPA itp
No i generalnie to antypattern mieszać logikę z frameworkiem, rdzeń aplikacji powinien być raczej odporny na frameworki, no chyba że piszemy jakieś proste CRUDy, tylko że do tego już DDD nie potrzebujemy

chodziło mi o bardziej złożoną sytuację gdzie np. instancja jest przekazywana wiele razy do różnych metod (wymyślam).

Opcje są 2 - albo nie zmieniasz argumentu i nie istnieje żaden problem, albo zmieniasz i wtedy nie masz czystych funkcjji i to że tak chcesz zrobić jest problemem.

Jasne, pod warunkiem że mamy niezmienne kolekcje.

Java - Vavr, Kotlin -ArrowKt

Czy kopiowanie złożonych obiektów po każdej operacji we wszystkich klasach nie zaciemnia logiki, mam na myśli stosunek ilości kodu poświęconego na kopiowanie do ilości kodu właściwej logiki?

A czy settery nie zaciemniają? Kopiowanie ułatwia de facto ogarniecie kodu, polecam:

0

Oczywiście że ma, pomiając już ten bezargumentowy konstruktor (bardzo słabe) to jeszcze jest cykl życia encji, różne jakieś dziwne stany, proxy JPA itp

Na poziomie warstwy domeny obiekty zachowują się jak zwykle POJO, z czymś takim jak cykl życia encji (jeśli chodzi Ci o encję w kontekście JPA, a nie DDD) mamy do czynienia dopiero na poziomie warstwy aplikacji więc nie wiem jaki wpływ na logikę biznesową, oprócz tego który wymieniłem wcześniej masz na myśli. W warstwie domeny istnieje jedynie abstrakcja repozytoriów implementowana przez warstwę niżej.

A czy settery nie zaciemniają? Kopiowanie ułatwia de facto ogarniecie kodu, polecam:

W DDD, jak może zauważyłeś w moim kodzie nie stosuje się głupich getterów i setterów zwracających i ustawiających pola, które nie zawierają żadnej logiki, tylko metody, które w jasny sposób opisują daną czynność, więc w żadnym stopniu nie zaciemniają one logiki biznesowej i dodatkowo powinny być atomowe czyli po wykonaniu dowolnej operacji stan systemu powinien być spójny, np. changePassword(String newPassword) czy submitSomething(Object something);

1

mamy do czynienia dopiero na poziomie warstwy aplikacji więc nie wiem jaki wpływ na logikę biznesową, oprócz tego który wymieniłem wcześniej masz na myśli. W warstwie domeny istnieje jedynie abstrakcja repozytoriów implementowana przez warstwę niżej.

Ma to że nie jest to już obiekt domenowy niezależny od od warstwy perzystencji tylko jest zakażona adnotacją JPA, jest juz obiektem proxy. Jeśli już stosować rakotwórcze JPA to w wartstwie infrastruktury

W DDD, jak może zauważyłeś w moim kodzie nie stosuje się głupich getterów i setterów zwracających i ustawiających pola,

No dobrze, w czym to:

issue.assignee(userId)

jest lepsze od:


val assignedIssue = issue.assignee(userId)
0

Ma to że nie jest to już obiekt domenowy niezależny od od warstwy perzystencji tylko jest zakażona adnotacją > JPA, jest juz obiektem proxy. Jeśli już stosować rakotwórcze JPA to w wartstwie infrastruktury

Jasne, o adnotacjach pisałem wyżej. W pełni się zgadzam, wiedza o ORM w tym wypadku jest zawarta w obiektach domeny jednak nie ma to żadnego wpływu na logikę biznesową, obiekt po usunięciu adnotacji będzie się zachowywać dokladnie tak samo, obiektem proxy staje się dopiero w warstwie aplikacji kiedy do gry rzeczywiście wkracza jpa.
Nie ma nic za darmo, jeśli komuś bardzo zależy na JPA i jak napisałeś braku "zakażonych" obiektów, które zawierają kilka adnotacji to można wykorzystać starą konfigurację w xmlu bez użycia adnotacji.

No dobrze, w czym to:

issue.assignee(userId)
jest lepsze od:

val assignedIssue = issue.assignee(userId)

Nigdzie nie napisałem, że coś jest lepsze lub gorsze, każde rozwiązanie ma swoje wady i zalety jednak uważam, że nie należy stosować jednego lub drugiego rozwiąnia na siłe tam gdzie nie ma takiej potrzeby.

1

Co stoi na przeszkodzie wyizolować JPA za pomocą portów i adapterów (pomijając czy ma to sens w danej aplikacji)? Wtedy repozytoria zwracają i przyjmują obiekty domenowe (agregaty, encje), które są mapowane na te znienawidzone (zapewne z wzajemnością) encje JPA.

0

@Charles_Ray: Może nakład pracy związany z Twoim rozwiązaniem, a usunięciu adnotacji JPA nawet z dużego projektu w przypadku zmiany dostawcy.

1

Nie ma nic za darmo, jeśli komuś bardzo zależy na JPA i jak napisałeś braku "zakażonych" obiektów, które zawierają kilka adnotacji to można wykorzystać starą konfigurację w xmlu bez użycia adnotacji.

Omg, to wiele nie zmienia, nadal jest to proxy itd

, obiektem proxy staje się dopiero w warstwie aplikacji kiedy do gry rzeczywiście wkracza jpa.

Z tego co wiem wystarczy że jest zwracane z EnityManagera

Nigdzie nie napisałem, że coś jest lepsze lub gorsze, każde rozwiązanie ma swoje wady i zalety jednak uważam, że nie należy stosować jednego lub drugiego rozwiąnia na siłe tam gdzie nie ma takiej potrzeby.

To jakie wady w porówaniu to zalet ma stosowanie niemutowalnych obiektów domenowych? :)

które są mapowane na te znienawidzone (zapewne z wzajemnością) encje JPA.

@Charles_Ray alez po co używać aż takiego słownictwa. Może z tym rakotwórczym trochę przesadziłem :D
JPA może mieć zalety przy operacjach zapisu/aktualizacji/usuwania, ale jak już te JPA stosujesz to trzymaj "przy glebie", np. na poziomie repozytorium bo wyżej moga być problemy ;)

@eziomou
Polecam jeszcze:

0

@eziomou
Polecam jeszcze:

Dziękuję, na pewno zobaczę jak zorganizuje trochę czasu.

Nie ma nic za darmo, jeśli komuś bardzo zależy na JPA i jak napisałeś braku "zakażonych" obiektów, które zawierają kilka adnotacji to można wykorzystać starą konfigurację w xmlu bez użycia adnotacji.

Omg, to wiele nie zmienia, nadal jest to proxy itd

, obiektem proxy staje się dopiero w warstwie aplikacji kiedy do gry rzeczywiście wkracza jpa.

Z tego co wiem wystarczy że jest zwracane z EnityManagera

Cała nasza dotychczasowa wymiana zdań dotyczyła warstwy domeny, więc po raz kolejny piszę, że poza adnotacjami, JPA nie ma żadnego udziału w warstwie domeny w podejściu DDD, również EntityManager.

Nigdzie nie napisałem, że coś jest lepsze lub gorsze, każde rozwiązanie ma swoje wady i zalety jednak uważam, że nie należy stosować jednego lub drugiego rozwiąnia na siłe tam gdzie nie ma takiej potrzeby.

To jakie wady w porówaniu to zalet ma stosowanie niemutowalnych obiektów domenowych? :)

Pierwsze co mi przychodzi na myśl, o czym już wcześniej pisałem to konieczność zapewniania niezmienności w przypadku gdy jest to kompletnie zbędne.
Dodatkowo potencjalne wady dobrze wytłuszczyłeś w swoim artykule.

2

więc po raz kolejny piszę, że poza adnotacjami, JPA nie ma żadnego udziału w warstwie domeny w podejściu DDD, również EntityManager.

Czyli ma a nie powinno mieć. Wystarczy że wywalisz JPA z mavena i nagle Twoja domena się kompiluje, a tak nie powinno być, no chyba że zakładasz pisanie CRUDa, tylko wtedy nie stosujesz takich architektur jak DDD. Może trochę spędzisz czasu w życiu z JPA to wtedy zrozumiesz :)

Pierwsze co mi przychodzi na myśl, o czym już wcześniej pisałem to konieczność zapewniania niezmienności w przypadku gdy jest to kompletnie zbędne.

Musze Cię zmartwić, zasadniczo jest odwrotnie. Obiekt** powinien być ** niemutowalny chyba że istnieje jakaś racjonalna przyczyna żeby był mutowalny. Chyba że nie lubisz korzystać ze współbieżności, albo lubisz "brudne funkcje" a przez to psują jakość kodu. Przekazujesz argument i nie wiesz czy ktoś go nie będzie chcial zmienić więc ryzykujesz. Może lubisz długo debugować i wstawiać breakpointy co 2 linijki żeby sprawdzić co się stało.

W każdym razie pytałeś o zdanie bardziej doświadczonych programistów to się odezwałem.
PS
Polecam ksiązke "Clean Architecture"

0

@scibi92:

więc po raz kolejny piszę, że poza adnotacjami, JPA nie ma żadnego udziału w warstwie domeny w podejściu DDD, również EntityManager.

Czyli ma a nie powinno mieć. Wystarczy że wywalisz JPA z mavena i nagle Twoja domena się kompiluje, a tak nie powinno być...

Jasne, zgadzam się, do tej pory rozmawialiśmy o wpływie JPA na logikę.

Mógłbyś mi jeszcze napisać co Twoim zdaniem jest racjonalną przyczyną dla mutowalności oraz czy według Ciebie każda klasa niemutowalna lub metoda, która zmienia stan instancji klasy do której należy jest kodem złej jakości? Czy brak wymogu dotyczącego współbieżności dla danej klasy lub założenie projektowe dotyczące precyzyjnego odwzorowania modelowanej dziedziny (DDD) jest dla Ciebie wystarczającą przesłanką o mutowalności?

W każdym razie pytałeś o zdanie bardziej doświadczonych programistów to się odezwałem.

Dzięki za opinię!

1

Mógłbyś mi jeszcze napisać co Twoim zdaniem jest racjonalną przyczyną dla mutowalności

Naturalność lub wydajność.
Trudno mi sobie wyobrazić niemutowalne okienko w Swingu, czy niemutowalny strumień I/O.

? Czy brak wymogu dotyczącego współbieżności dla danej klasy

Nie,

precyzyjnego odwzorowania modelowanej dziedziny (DDD) jest dla Ciebie wystarczającą przesłanką o mutowalnośc

Niemutowalność jak najbardziej idzie w parze moim zdaniem z logiką biznesową, i dlatego dla mnie naturalne bardziej byłoby tworzenie logiki biznesowej z obiektami niemutowalnymi.
Przykłady gdzie widze sens dla mutowalności:
1)Rzeczy związane z I/O
2)Niektóre narządzia do wielowątkowści. Np. ciężko mi sobie wyobrazić niemutowalny ExecutorService albo CountDownLatch. Nie zmienia to faktu że nawet tam ogranicza się mutowalność do minimum.
3)GUI - dla mnie mutowalne okienko jest bardziej racjonalne niż niemutowalne
4)Cachowanie danych (tam często jest HashMapa wykorzystywana) w samej Javie
5)Kiedy znaczenie ma wydajność, np. low-latency - wtedy nie chcesz wywoływać zbyt często GC, no i też często dochodze rzeczy typu operacje na bajtach, off-heap memory itp

0

@scibi92:
Odświeżę temat i zadam jeszcze jedno pytanie. Przeczytałem poleconą
przez Ciebie książkę. ORM takie jak JPA może i wchodzą z butami do
warstwy domeny i wymagają tworzenia klas w określony sposób ale zwalniają
też z dużej ilości pracy. Czy istnieje jakaś alternatywa umożliwiająca
łatwe podpięcie bazy danych w której nie trzeba tworzyć oddzielnych modeli
domena/baza danych?

tutaj dodatkowo artykuł o tym jak kosztowne jest tworzenie oddzielnych modeli: link

0
eziomou napisał(a):

Czy istnieje jakaś alternatywa umożliwiająca
łatwe podpięcie bazy danych w której nie trzeba tworzyć oddzielnych modeli
domena/baza danych?

To nie jest przecież kwestia technologii tylko designu.

0
eziomou napisał(a):

Czy istnieje jakaś alternatywa umożliwiająca
łatwe podpięcie bazy danych w której nie trzeba tworzyć oddzielnych modeli
domena/baza danych?

Encja na twarz i pchasz
Przy małych projektach nawet się sprawdza. Przy większych to pułapka.

0

To nie jest przecież kwestia technologii tylko designu.

No nie do końca, bo jak na przykład korzystasz z ORM to ciężko sensownie mieć ten sam model domena/baza danych.

@danek @eziomou
To w sumie zależy, jak na przyklad masz JdbcTemplate i być może JOOQ (ale tu ręki nie dam sobie uciąć za to) to można de facto zrobić tak że robisz modele bazodanowe by default takie same jak modele domenowe, chyba że istnieje powód żeby tak nie robić :)
Ale jak się nie da to trudno, koszt mapowania nie powinien raczej być zbyt duży ;]

0
scibi92 napisał(a):

To nie jest przecież kwestia technologii tylko designu.

No nie do końca, bo jak na przykład korzystasz z ORM to ciężko sensownie mieć ten sam model domena/baza danych.

No o to właśnie chodzi, że taki design nigdy nie jest sensowny, nieważne jakiej technologii się użyje.

Ale jeśli podejmie się taką decyzję (aby mieć tylko jeden model) odnośnie architektury, to jedne technologie pomogą bardziej, inne mniej, ale finalnie zazwyczaj i tak się napotka na jakieś problemy albo wręcz ścianę. Jak zawsze w przypadku złamania SRP.

0

@scibi92: w teorii najpierw robisz design, a potem wybierasz technologie

0

@somekind @danek @scibi92

Dzięki za wypowiedzi.

Ale jeśli podejmie się taką decyzję odnośnie architektury, to jedne technologie pomogą bardziej, inne mniej, ale finalnie zazwyczaj i tak się napotka na jakieś problemy albo wręcz ścianę. Jak zawsze w przypadku złamania SRP.

Możesz wytłumaczyć gdzie tu jest łamana zasada SRP? Jeśli wykorzystujesz ORM i masz dwa modele to jeden reprezentuje reguły biznesowe, a drugi strukturę danych. Przykładem może być synchronizacja baz danych w podejściu CQRS w której przy skryptowej synchronizacji rekordy w bazie danych mogą potrzebować dodatkowych informacji takich jak data utworzenia lub modyfikacji rekordu, które to w encjach biznesowych wcale mogą nie być obecne. Jeśli przy stosowaniu ORM masz tylko jeden model i chcesz zmienić ORM lub bazę danych to musisz też zmienić encje, więc czy to nie w tym podejściu łamana jest zasada SRP?

2
eziomou napisał(a):

Jeśli przy stosowaniu ORM masz tylko jeden model i chcesz zmienić ORM lub bazę danych to musisz też zmienić encje, więc czy to nie w tym podejściu łamana jest zasada SRP?

SRP polega na tym, że klasa musi mieć tylko jeden powód do zmian. Jeśli jedna klasa jest jednocześnie domain modelem i persistence modelem, to ma dwa powody do zmian: zmiany w logice biznesowej lub zmiany w sposobie przechowywania danych.

0
somekind napisał(a):
eziomou napisał(a):

Jeśli przy stosowaniu ORM masz tylko jeden model i chcesz zmienić ORM lub bazę danych to musisz też zmienić encje, więc czy to nie w tym podejściu łamana jest zasada SRP?

SRP polega na tym, że klasa musi mieć tylko jeden powód do zmian. Jeśli jedna klasa jest jednocześnie domain modelem i persistence modelem, to ma dwa powody do zmian: zmiany w logice biznesowej lub zmiany w sposobie przechowywania danych.

Jasne, wydawało mi się że napisałeś odwrotnie

0

Skorzystam z okazji i zadam jeszcze jedno pytanie. W jaki sposób
powinny być realizowane transakcje? Wydaje się, że są szczegółem więc
powinny zostać zaimplementowane na poziomowe warstwy danych i gwarantować
atomowość metodom repozytoriów, jednak większość przypadków użycia
wykorzystuje więcej niż jedno repozytorium lub inne przypadki użycia.

0

Repozytorium to nawet nie powinno mieć pojęcia o czymś takim jak transakcje.
W przypadku programowania z repozytoriami, to transakcje powinny być na poziomie Unit of Work.

Ale Ty masz przecież simple REST web service. Po co tam w ogóle repozytoria?

0

Cześć ostatnio miałem trochę wolnego czasu więc zrobiłem refaktoryzację do Clean Architecture, jeśli ktoś jest ciekawy lub zwyczajnie chce ocenić/skrytykować, zapraszam na githuba.
Co zrobiłem to między innymi zastosowałem się do sugestii powyżej (próba połączenia niezmiennych modeli i języka wszechobecnego) i całkowicie wyrzuciłem JPA oraz podzieliłem kod na sensowne moduły. library-backend

1 użytkowników online, w tym zalogowanych: 0, gości: 1