Spring service dla wielu tabel

0

Piszę rest api do obsługi helpdesk w springu.
https://github.com/konradszafranski/helpdesk-backend

Chciałbym teraz dodać encję "sprawa serwisowa" nie za bardzo wiem jak to zrobić żeby było to napisane w miarę sensownie i czysto.

Encja sprawa serwisowa powinna być w relacji z osobą która utworzyła sprawę i tą która aktualnie ją obsługuje, myślę o tym żeby zamiast wrzucać wszystkich użytkowników do jednej tabeli utworzyć dla każdego typu pracownika klasę. Dzięki temu rekordy z tabeli users nie będą wchodziły ze sobą pośrednio w relacje i będzie można stworzyć unikalny zbiór uprawnień dla każdego z typów.

@MappedSuperclass
public abstract class User implements UserDetails {
   // implementacja UserDetails
}

@Entity
@Table(name = "customerService")
public class CustomerService extends User {
   //List<CustomerServicePermission> up;
   //List<Utworzone sprawy> us; 
}

@Entity
@Table(name = "serviceTech")
public class ServiceTech extends User {
   //List<ServiceTechPermission> up;
   //List<Utworzone sprawy> us; 
   //List<Obsługiwane sprawy> os;
}

@Entity
@Table(name = "admin")
public class Admin extends User {
   //List<AdminPermission> up;
   //List<Obsługiwane sprawy> os;
}

Zastanawiam się czy w takiej sytuacji dla każdego typu użytkownika powinno się utworzyć odpowiednio Service?
Bo jeśli nie to klasa UserService kożystałaby conajmniej z 3 repozytoriów - CustomerServiceRepo, ServiceTechRepo, AdminRepo i musiałaby każdorazowo określać jakiego typu jest użytkownik na którym należy wykonać działanie.

@Service
public class UserService implements UserDetailsService {

    EmployeeRepo employeeRepository;
    UserPermissionRepo userPermissionRepository;
    EmployeeWrapper employeeWrapper;
    PasswordEncoder passwordEncoder;

    public UserService(EmployeeRepo employeeRepo, UserPermissionRepo userPermissionRepo, EmployeeWrapper employeeWrapper, PasswordEncoder passwordEncoder) {
        this.employeeRepository = employeeRepo;
        this.userPermissionRepository = userPermissionRepo;
        this.employeeWrapper = employeeWrapper;
        this.passwordEncoder = passwordEncoder;
    }

    public void createNewUser(CreateUserRequest createUserRequest) {
        Employee employee = employeeWrapper.employeeRequestToEmployee(createUserRequest);
        employee.setPassword(passwordEncoder.encode(employee.getPassword()));
        try {
            employeeRepository.saveAndFlush(employee);
        } catch (Exception e) {
            System.out.println("exception " + e.getMessage() + " " + e.getCause());
        }
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            return employeeRepository.findUserByName(username);
        } catch (Exception e) {
            System.out.println(e.toString());
            throw e;
        }
    }

    public List<Employee> getListOfUsers() throws HibernateException {
        List<Employee> employeeList = employeeRepository.findAll();
        return  employeeList;
    }

    @PostConstruct
    public void createSampleUser() {
        try {
            UserPermission userRole = new UserPermission("GET_ALL_CASES");
            userPermissionRepository.save(userRole);
            UserPermission userPermission2 = new UserPermission("ADD_EMPLOYEE");
            userPermissionRepository.save(userPermission2);

            Set<String> userPermissions = new HashSet<>();
            userPermissions.add("ADD_EMPLOYEE");
            CreateUserRequest createUserRequest = new CreateUserRequest("k_sz",
                    "cat",
                    userPermissions,
                    "name",
                    "surname",
                    "POPO",
                    "544190270");
            createNewUser(createUserRequest);
        } catch (Exception e) {
            System.out.println("employeeService");
            System.out.println(e);
        }
    }
}

Czy w takiej sytuacji np. metoda createNewUser musiałaby być albo przeciążona dla każdego typu użytkownika albo trzeba byłoby sprawdzać typ za pomocą switch albo instanceof żeby wiadomo było do której z tabel zapisać danego użytkownika?
Nie wiem czy to jest poprawne podejście ponieważ jeśli każda metoda będzie albo w 3 egzemplarzach albo będzie ustalała typ to klasa może się mocno rozrosnąć wraz ze zwiększaniem liczby metod.

Natomiast metoda @Override loadUserByUsername(String username), sprawdzałaby każdorazowo wszystkie tabele w poszukiwaniu odpowiedniego użytkownika ponieważ jedyny jej argument to "String username"

W jaki sposób można byłoby to sensownie zaprojektować?

1

Zwróć uwagę:
temat zakładasz "spring service", pojawia się REST, masz tok myślenia "all in one" a naprawdę chodzi o projekt encji biznesowej (nie bazodanowej *)` )
A z biznesowej jej krewniak jakieś DTO

*"Spring service dla wielu tabel" *
na wyższych warstwach w ogóle nie myślimy tabelami

Dziel zagadnienia, dobrze na tym wyjdziesz.

*) w jakimkolwiek nieco bardziej skomplikowanym zagadnieniu encja bazodanowa jest złym materiałem, aby myśleć o niej na sposób architektoniczny. Właśnie wynalazłeś przykład.
A w każdym projekcie "pro" jest ukryta w warstwie perzystencji i nie występuje wyżej.

4

w miarę sensownie i czysto.

A potem jakieś copypasty z d**y, jakeiś dziedziczenie hibernatowych @Entity, jakieś duplikowane serwisy, jakieś DAO które mylnie nazywasz repozytoriami. To jest dramat. Wyłączyłeś myślenie i próbujesz na siłę cudować frameworkami i adnotacjami.

  1. Zaprojektuj ludzką bazę danych - masz tabele Task która ma reporter_id i handler_id, oba z tabeli Users. Oprócz tego możesz zrobić sobie tabele Permissions i tabele M:N User_Permissions. Dzięki temu masz banalny model danych który w trywialny sposób daje ci wszystko czego potrzebujesz. Hibernate to rak, ale jak bardzo chcesz to możesz spokojnie spiąć sobie hibernate z bazą o takim schemacie bo jest prosty jak budowa cepa. Nie ma to sensu, bo zaraz to Entity trzeba będzie przemapować na obiekt domenowy, ale niektórym płacą od linijki kodu.
  2. Robisz biznesowe repozytoria które zwracają obiekty domenowe. Ja tu widzę jeden obiekt User który będzie miał listę założonych spraw, listę obsługiwanych spraw i listę uprawnień. Zbudowanie takiego obiektu jest banalne i wymaga raptem 3 zapytań do bazy danych. Więc masz jedno repozytorium które zwraca ci użytkownika. Jak bardzo chcesz to możesz wydzielić osobne klasy po jednej na każde z tych zapyta
  3. Robisz biznesowy serwis który korzysta z tego repozytorium i triggeruje jakieś biznesowe operacje na użytkownikach

Czasem zastanawia mnie jak to jest, że ludzie jak sie uczą Javy/OOP to ładnie lecą na przykładach z kotkami, pieskami czy instrumentami i potrafią napisać kod w którym maja ładne obiekty które ze sobą współpracują. A potem nagle przeskakują na robienie jakiegoś proceduralnego cyrku i robią struktury danych produkowane przez hibernate i serwisy springowe i mają oddzielone operacje od danych, jak w starym dobrym C :D

4

Jak już inni napisali: zadałeś to pytanie w taki sposób, że ciężko uwierzyć, że rozumiesz, co robisz.

Prawdopodobnie masz dwa problemy:

  • Mapowaniem dziedziczenia w JPA. Hasło dla google: "jpa inhertance mapping".
  • Przepychanie tego przez warstwy wyżej.
    To trochę pytanie podstawowe - "jak stosować abstrakcję w Javie?". Jest mnóstwo technik. Bez ich poznania nie powinno się ludzi dopuszczać do Springa czy - o zgrozo - Hibernate. Jeżeli nie umiesz tego zrobić dla POJO, gdzie "encje" wyciągasz z in-memory repozytoriów, to tym bardziej nie będziesz tego umiał zrobić w JPA + spring-data-jpa + Spring + REST. Tu próbujesz zrobić coś co przypomina generic-factory+generic repository na "wartswie @Service" - może jakaś abstrakcyjna implementacja template? Ale uwaga! Takie zgrupowanie metod dotyczących jednego typu danych w god-object singleton "Service" jest podejrzane. Twój problem to "sprawa serwisowa", a zastanawiasz się czy tworzyć serwis dla każdego typu użytkownika. Wydaje mi się, że dodajesz warstwę architektoniczną na siłę.
0

@Shalom:

niektórym płacą od linijki kodu

Mi jeszcze nigdy nikt nie załacił za kod. Może za kilka miesięcy jak dobrze pójdzie to na jakimś stażu dostanę swoje pierwsze parę złotych.

Ja tu widzę jeden obiekt User który będzie miał listę założonych spraw, listę obsługiwanych spraw i listę uprawnień.

Ok tylko zakładając że część użytkowników (w zależności od roli) będzie miała możliwość jedynie zakładania spraw to korzystając ze wspólnego obiektu User i tak posiadaliby oni atrybut "obsługiwane sprawy".

@Entity
@Table(name = "user")
public class User {
   //List<User Uprawnienia> uu;
   //List<Utworzone sprawy> us; 
   //List<Obsługiwane sprawy> os;
}

Czyli w takiej sytuacji należałoby sterować tym z poziomu logiki, np. utworzyć parametr "Rola" i w zależności od jego wartości umożliwiać lub zabraniać przypisanie wartości do "obsługiwane sprawy"?
Tak samo w przypadku uprawnień które może mieć admin ale serwisant już nie.

@AnyKtokolwiek:

A już myślenie Hibernatem w warstwach wyższych, np propagowanie Hibernate Exception, to się scyzoryk otwiera

Czyli dobrze rozumiem że stosując zasadę abstrakcji powinno się albo rzucić bardziej ogólny wyjątek (PersistenceException, RuntimeException) ukrywając szczegóły implementacji (warstwy service, controller nie muszą wiedzieć np. że to hibernate obrabia persystencje) przed wyższą warstwą albo obsłużyć wyjątek niżej jedynie zwracając wynik działania jako np. boolean? W ogóle to nie za bardzo rozumiem czym należy się kierować podejmując decyzję o tym kiedy obsłużyć wyjątek a kiedy propagować go wyżej. Są jakieś zasady, dobre praktyki dotyczące tego zagadnienia?

@ArchitektSpaghetti

To trochę pytanie podstawowe - "jak stosować abstrakcję w Javie?

No właśnie czuję że mam z tym problem. Może nie w sensie co to jest klasa abstrakcyjna, interfejs bo tyle to wiem ale z punktu widzenia celowości stosowania abstrakcji w projekcie. Polecilibyście jakieś materiały albo książkę która dobrze wyjaśnia (najlepiej na przykładach) takie kwestie jak np. "kiedy stosować abstrakcję", "po co stosować abstrakcję", "dependency inversion principle", "interface segregation principle"?

4

Ok tylko zakładając że część użytkowników (w zależności od roli) będzie miała możliwość jedynie zakładania spraw to korzystając ze wspólnego obiektu User i tak posiadaliby oni atrybut "obsługiwane sprawy".

NIE NIE NIE. Nadal myślisz jakimiś krzywymi klasami Entity które w ogóle nie są ci do niczego potrzebne. Zapomnij o nich na chwile zupełnie i zacznij od zamodelowania obiektów domenowych z którymi chcesz pracować w aplikacji. Bo teraz to się za to zabierasz od d**y strony. Załóżmy że chcesz mieć w domenie np. coś takiego:

class Handler{
    private final List<Privilege> privileges;
    private final List<ManagedCase> cases;

    public ManagedCase resolveCase(UUID caseId){}
}

class ManagedCase{
    private final Case case;
    private final User initiator;
}

class User{
    private final List<Case> cases;

    public Case createCase(InputsDTO inputs){}
}

I teraz taki obiekt Handler miałby jakieś metody domenowe w stylu updateCase, resolveCase, a User miałby jakieś createNewCase. Ma to sens? To teraz zrób Repozytorium które zwraca Usera i takie które zwraca Handlera, bo zakładam ze to są jakieś rooty w twojej aplikacji. Jak sobie te obiekty zbudujesz to jest twoja sprawa, czy SQLem czy Hibernatem czy czymkolwiek innym. Tylko broń Boże nie próbuj do tych klas dodawać jakichś adnotacji! Jak chcesz się bawić w hibernata to nastukaj te hibernatowe DTO a potem w repozytorium przemapujesz je na te obiekty domenowe.

Generalnie nie traktuj Hibernate jako jakiegoś kluczowego elementu. Kluczowy element to jest logika domenowa aplikacji, a warstwa trwałego zapisu danych to jest tylko jakiśtam element, porównywalny np. z wysłaniem maila z notyfikacją jak twoja sprawa została rozwiązana. A ty próbujesz z jakiegoś powodu wyjść od tego maila z notyfikacją i zbudować dookoła niego cała resztę systemu :)

1
happyTaco napisał(a):

@AnyKtokolwiek:

A już myślenie Hibernatem w warstwach wyższych, np propagowanie Hibernate Exception, to się scyzoryk otwiera

Czyli dobrze rozumiem że stosując zasadę abstrakcji powinno się albo rzucić bardziej ogólny wyjątek (PersistenceException, RuntimeException) ukrywając szczegóły implementacji (warstwy service, controller nie muszą wiedzieć np. że to hibernate obrabia persystencje) przed wyższą warstwą albo obsłużyć wyjątek niżej jedynie zwracając wynik działania jako np. boolean? W ogóle to nie za bardzo rozumiem czym należy się kierować podejmując decyzję o tym kiedy obsłużyć wyjątek a kiedy propagować go wyżej. Są jakieś zasady, dobre praktyki dotyczące tego zagadnienia?

W twoim pytaniu są dwa odrębne, każdy godny eseju:

  • jaką klasę wyjątku rzucać. Tu są pewne zmiany historyczne, w Javie wydawało się, że miodem będą checked exceptions, ale życie pokazało co innego. Jak romansuję z C#, to programiści mówią "bo wy jawowcy za dużo powołujecie klas wyjątków"
  • gdzie go łapać.
    Przenigdy wynik jako bolean ani inetger. Właśnie to jest rewolucyjne w wyjątkach, że odchodzi od wzorców z "C" z lat 1960-70. To się tak kończy, że każde niewodzenie to komunikat "brak miejsca na dysku"
    (za wczesne łapanie, głupie, bo nie ma dogodnego kontekstu, to śmieszne drukowania na sysout itd -> na aplikacji leci stacktrace, po czym komunikat "operacja wykonana poprawnie")

Puszczasz wyjątek tak daleko (wysoko), aż będzie dobry kontekst by to zrobić, aż coś z tego wynika, np zerwanie algorytmu, danie jednoznacznej sytuacji np użytkownikowi
Nadmienię, że zerwana może byś EE lub Springowa transakcja (ale to apage satanas, oswój projektowanie i normalną Javę SE)

1

@happyTaco:

Nie przesadzajmy z tym pakowaniem wyjątków. Możesz rzucić HibernateWhateverException, jeżeli oznacza to niespodziewany błąd wykonania. Ważne żeby to, co go łapie nie zależało od Hibernate, ale po prostu operowało na abstrakcji RuntimeException. Abstrakcja nie musi oznaczać, że na pewno się nie da dobrać do wnętrza, ale że użytkownicy operują na interfejsie.

Z drugiej strony, jeżeli spodziewasz się, że dany wyjątek Hibernate ma jakieś znaczenie domenowe, to go trzeba przetłumaczyć. np RowNotFoundException na jakiś UserNotFound czy Optional.empty().

Co do Either: kwestia stylu - niektórzy lubią, niektórzy nie. Są fajne narzędzia do funkcyjnego składania tego w chainy, ale z drugiej strony to przywraca problemy z nadmiernym rozpisywaniem, jakie występowały w czasach checked exceptions. (No po prostu musisz obsłużyć IOException, jak parsujesz Jsona ze Stringa, i wklejać to wszędzie, bo ktoś kiedyś tak zaprojektował api InputStreama).

0

@Shalom:

I teraz taki obiekt Handler miałby jakieś metody domenowe w stylu updateCase, resolveCase, a User miałby jakieś createNewCase. Ma to sens? To teraz zrób Repozytorium które zwraca Usera i takie które zwraca Handlera, bo zakładam ze to są jakieś rooty w twojej aplikacji. Jak sobie te obiekty zbudujesz to jest twoja sprawa, czy SQLem czy Hibernatem czy czymkolwiek innym.

To mam jeszcze takie pytanie. Zakładając że ten Case na jakimś etapie będzie jednak zapisywany/odczytyany z bazy danych to zastanawiam się jak powinny działać metody createNewCase, updateCase, resolveCase.

class Handler{
    private final List<Privilege> privileges;
    private final List<ManagedCase> cases;

    public ManagedCase resolveCase(UUID caseId){
        // od czego zależe?
    }
}

class ManagedCase{
    private final Case case;
    private final User initiator;
}

class User{
    private final List<Case> cases;

    public Case createCase(InputsDTO inputs){
       // od czego zależe?
    }
}

Czy te klasy mogą korzystać z repozytorium/dao które obsługuje Case?
Bo jeśli tak to rozumiem że User/Handler zaczynają zależeć od repozytorium albo jakiejś jego abstrakcji.
Czy powinno się to zrobić odwracając zależność czy w jakiś inny sposób?

Znalazłem na githubie projekt w którym autor zaimplementował coś w oparciu o DDD.
https://github.com/ChristophKnabe/spring-ddd-bank/tree/45dc3f2c8e73c39aca4ffdeaadb0f0f5b40ae6c0/src/main/java/de/beuth/knabe/spring_ddd_bank

Tutaj klasa User ma wstrzyknięty interface AccountRepo przy czym jest komentarz "Required repositories as by Ports and Adapters Pattern:"
https://github.com/ChristophKnabe/spring-ddd-bank/blob/45dc3f2c8e73c39aca4ffdeaadb0f0f5b40ae6c0/src/main/java/de/beuth/knabe/spring_ddd_bank/domain/Client.java

To miałoby wyglądać mniej więcej tak jak w tym projekcie?

2

Bo jeśli tak to rozumiem że User/Handler zaczynają zależeć od repozytorium albo jakiejś jego abstrakcji.

To zależy, można to zrobić na 2 sposoby:

  • albo zależysz od jakiegoś interfejsu (niemniej sugerowałbym dużo węższy niż całe repozytorium, prędzej jakieś Function<Data,Cośtam>), to szczególnie może być potrzebne jeśli masz "agregacje"/"asocjacje" a nie "kompozycje", tzn w tym swoim obiekcie masz referencje do czegoś czego nie kontrolujesz
  • albo na poziomie obiektu domenowego robisz tylko operacje "w pamięci" i wszelkie potrzebne walidacje i zwracasz jako wynik Either<Error,Cośtam> a zapisanie efektów w jakiejś bazie danych przerzucasz do "serwisu" który obsługuje ten use-case, czyli miałbyś jakieś:
class CreateCaseUC{
//
    public Either<Error,Case> createNewCase(UserId requesterId, InputsDTO inputs){
        return usersProvider.getUser(requesterId)
            .flatMap(user -> user.createCase(inputs))
            .flatMap(newCase -> casesRepository.persist(newCase));
    }
}

ta druga opcja jest ciekawa jeśli np. potrzebujesz robić kooperacje pomiędzy różnymi obiektami domenowymi, bo "gdzieś" to trzeba zrobić. Dodatkowo w tej sytuacji obiekt domenowy faktycznie skupia sie tylko na "domenie" a technikalia jak to czy jakieśtam zmiany lecą do repozytorium czy nie, wyciągasz w inne miejsce.

Niemniej jeśli potrzebujesz jakąś odpowiedź zgodną z dogmatami DDD to nie jestem najlepszą osobą żeby ci jej udzielić, musisz poszukać jakiegoś ewangelisty :)

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