2016-08-19 44 views
8

Zasadniczo staram się zrozumieć, jak napisać prawidłowy (lub "poprawnie napisać"?) Kod transakcyjny, przy tworzeniu usługi REST z Jax-RS i Spring . Ponadto używamy JOOQ do dostępu do danych. Ale to nie powinno być zbyt istotne ...
Rozważmy prosty model, w którym mamy pewne organizacje, które mają następujące pola: "id", "name", "code". Wszystkie muszą być unikatowe. Jest też pole status.
Organizacja może zostać w pewnym momencie usunięta. Ale nie chcemy całkowicie usuwać danych, ponieważ chcemy je zapisać do celów analitycznych/konserwacyjnych. Dlatego właśnie ustawiliśmy pole statusu organizacji na 'REMOVED'.
Ponieważ nie usuwamy wiersza organizacji z tabeli, nie możemy po prostu wstawić wyjątkowego ograniczenia do kolumny "nazwa", ponieważ możemy usunąć organizację, a następnie utworzyć nową o tej samej nazwie. Ale załóżmy, że kody muszą być unikalne globalnie, więc mamy unikalne ograniczenie w kolumnie code.Jak napisać poprawny/niezawodny kod transakcyjny za pomocą JAX-RS i Spring

Zobaczmy więc ten prosty przykład, który tworzy organizację, przeprowadzając pewne kontrole po drodze.

zasobów:

@Component 
@Path("/api/organizations/{organizationId: [0-9]+}") 
@Consumes(MediaType.APPLICATION_JSON) 
@Produces(MediaTypeEx.APPLICATION_JSON_UTF_8) 
public class OrganizationResource { 
    @Autowired 
    private OrganizationService organizationService; 

    @Autowired 
    private DtoConverter dtoConverter; 

    @POST 
    public OrganizationResponse createOrganization(@Auth Person person, CreateOrganizationRequest request) { 

     if (organizationService.checkOrganizationWithNameExists(request.name())) { 
      // this throws special Exception which is intercepted and translated to response with 409 status code 
      throw Responses.abortConflict("organization.nameExist", ImmutableMap.of("name", request.name())); 
     } 

     if (organizationService.checkOrganizationWithCodeExists(request.code())) { 
      throw Responses.abortConflict("organization.codeExists", ImmutableMap.of("code", request.code())); 
     } 

     long organizationId = organizationService.create(person.user().id(), request.name(), request.code()); 
     return dtoConverter.from(organization.findById(organizationId)); 
    } 
} 

serwis DAO wygląda tak:

@Transactional(DBConstants.SOME_TRANSACTION_MANAGER) 
public class OrganizationServiceImpl implements OrganizationService { 
    @Autowired 
    @Qualifier(DBConstants.SOME_DSL) 
    protected DSLContext context; 

    @Override 
    public long create(long userId, String name, String code) { 
     Organization organization = new Organization(null, userId, name, code, OrganizationStatus.ACTIVE); 
     OrganizationRecord organizationRecord = JooqUtil.insert(context, organization, ORGANIZATION); 
     return organizationRecord.getId(); 
    } 

    @Override 
    public boolean checkOrganizationWithNameExists(String name) { 
     return checkOrganizationExists(Tables.ORGANIZATION.NAME, name); 
    } 

    @Override 
    public boolean checkOrganizationWithCodeExists(String code) { 
     return checkOrganizationExists(Tables.ORGANIZATION.CODE, code); 
    } 

    private boolean checkOrganizationExists(TableField<OrganizationRecord, String> checkField, String checkValue) { 
     return context.selectCount() 
       .from(Tables.ORGANIZATION) 
       .where(checkField.eq(checkValue)) 
       .and(Tables.ORGANIZATION.ORGANIZATION_STATUS.ne(OrganizationStatus.REMOVED)) 
       .fetchOne(DSL.count()) > 0; 
    } 
} 

To przynosi jakieś pytania:

  1. powinienem umieścić @Transactional adnotacji na createOrganization metody zasobu? A może powinienem stworzyć jeszcze jedną usługę, która rozmawia z DAO i wstawi adnotację @Transactional do swojej metody? Coś innego?
  2. Co by się stało, gdyby dwóch użytkowników jednocześnie wysłało żądanie z tym samym polem "code". Przed dokonaniem pierwszej transakcji kontrole zostaną pomyślnie zakończone, więc nie zostanie wysłanych 409 odpowiedzi. Niż pierwsza transakcja zostanie zatwierdzona poprawnie, ale druga będzie naruszać ograniczenie DB. Spowoduje to wygenerowanie wyjątku SQLException. Jak z wdziękiem sobie z tym poradzić? Chodzi mi o to, że nadal chcę wyświetlać miły komunikat o błędzie po stronie klienta, mówiąc, że nazwa jest już używana. Ale nie mogę naprawdę parsować SQLException lub czegoś ... czy mogę?
  3. Podobny do poprzedniego, ale tym razem "nazwa" nie jest unikalna. W takim przypadku druga transakcja nie naruszy żadnych ograniczeń, co prowadzi do posiadania dwóch organizacji o tej samej nazwie, które naruszają nasze ograniczenia biznesowe.
  4. Gdzie mogę zobaczyć/dowiedzieć się tutoriale/kod/etc., Które uważają za świetne przykłady, jak napisać poprawny/niezawodny kod REST + DB ze skomplikowaną logiką biznesową. Github/książki/blogi, cokolwiek. Próbowałem znaleźć coś takiego, ale większość przykładów skupia się na instalacjach - dodaj te biblioteki do maven, użyj tych adnotacji, jest twój prosty CRUD, koniec. W ogóle nie zawierają żadnych względów transakcyjnych. To znaczy.

UPDATE: wiem o poziomie izolacji i zwykle error/isolation matrix (brudny czyta, etc ..). Problemem jest znalezienie "gotowej do produkcji" próbki do nauki. Albo dobra książka na ten temat. Nadal nie wiem, jak poprawnie obsłużyć wszystkie błędy. Przypuszczam, że muszę powtórzyć kilka razy, jeśli transakcja się nie powiodła ... a następnie wystarczy podać ogólny błąd i zaimplementować klienta, który to obsługuje .. Ale nie Czy muszę używać trybu SERIALIZABLE, kiedy używam zapytań o zakres? Ponieważ znacznie wpłynie to na wydajność. Ale w inny sposób, w jaki sposób mogę zagwarantować, że ta transakcja się nie powiedzie.

Zresztą Zdecydowałem, że teraz muszę więcej czasu, aby dowiedzieć się o transakcji i zarządzania db w ogóle do rozwiązania tego problemu ...

Odpowiedz

-1

pierwsze warstwy DAO nie powinien nawet wiedzieć, że to jest frontem przez Usługa internetowa REST. Pamiętaj o oddzieleniu obowiązków.

Zachowaj @Transactional na DAO. Jeśli wydajesz tylko jedno zdanie, to musisz zdecydować, czy jesteś w porządku z brudnymi odczytami. Zasadniczo, wymyśl, jaki jest najniższy poziom izolacji dla twojej aplikacji. Każda metoda rozpocznie nową transakcję (o ile nie zostanie wywołana z innej metody, która już została uruchomiona) i jeśli zostaną wyrzucone jakiekolwiek wyjątki, przywróci wszystkie połączenia. Możesz skonfigurować niestandardowy ExceptionHandler w swoim kontrolerze, aby obsługiwać SQLDataIntegrityExceptions (np. Przykład wstawiania "kodu").

użyć klucza Zbiorczy podstawowy, który obejmuje (id, nazwa, kod, status), dzięki czemu można mieć org o tej samej nazwie, ale jeden będzie „CURRENT”, a jeden będzie „usunięte”

+0

A co, jeśli istnieją inne ważne statusy zawieszony, etc? Chodzi o to, że nie mogę tego zrobić z prostymi ograniczeniami ... Czy powinienem zawsze pisać skomplikowane wyzwalacze dla każdego złożonego ograniczenia, a więc powielać to, co napisałem w Javie? Również, jak z wdziękiem odzyskać od naruszania tych ograniczeń? I gdzie mogę znaleźć dobry przykład kodu, który następuje po tych praktykach? –

+0

Zakres transakcji to jednostka pracy, jednostka pracy nie jest zdefiniowana na poziomie DAO, ogólnie rzecz biorąc adnotacje @Transactionnal na poziomie DAO to zapach projektu – Gab

+0

@Gab Bardzo nie zgadzam się - jak sądzisz, gdzie powinna być zawarta informacja o transakcji zdefiniowane, jeśli nie tak blisko miejsca interakcji z bazą danych? – Gandalf

1

Generalnie nie mówiąc o transakcyjności, punkt końcowy powinien pobierać tylko parametry z żądania i wywoływać usługę. Nie powinna robić logiki biznesowej.

Wygląda na to, że metody checkXXX są częścią logiki biznesowej, ponieważ powodują błędy w konfliktach związanych z domenami. Dlaczego nie umieścić ich w Serwisie w jedną metodę, która jest na swój sposób transakcyjna?

//service code 
public Organization createOrganization(String userId, String name, String code) { 

    if (this.checkOrganizationWithNameExists(request.name())) { 
     throw ... 
    } 

    if (this.checkOrganizationWithCodeExists(code)) { 
     throw ... 
    } 

    long organizationId = this.create(userId, name, code); 
    return dao.findById(organizationId); 
} 

Wziąłem jako parametry są ciągi znaków, ale mogą być wszystko. Nie jestem pewien, czy chcesz rzucić Responses.abortConflict w warstwie usługi, ponieważ wydaje się, że jest to koncepcja REST, ale możesz zdefiniować dla niej własne typy wyjątków, jeśli chcesz.

Endpoint kod powinien wyglądać tak, jednak może on zawierać dodatkowy blok try-catch, który konwertuje rzucane wyjątki Błąd odpowiedzi:

//endpoint code 
@POST 
public OrganizationResponse createOrganization(@Auth Person person, CreateOrganizationRequest request) { 
    String code = request.code(); 
    String name = request.name(); 
    String userId = person.user().id(); 
    return dtoConverter.from(organizationService.createOrganization(userId, name, code)); 
} 

Co do pytania 2 i 3, transaction isolation levels są twoi przyjaciele. Umieść wystarczająco wysoki poziom izolacji. Myślę, że "powtarzalna lektura" jest odpowiednia w twoim przypadku. Metody checkXXX wykryją, czy jakaś inna transakcja zatwierdza podmioty o tej samej nazwie lub kodzie i jest zagwarantowane, że sytuacje pozostaną do czasu wykonania metody "create". Jeszcze jeden useful read w odniesieniu do poziomów izolacji Spring i transakcji.

+0

Ale nadal nie pomaga rozwiązać problemu z niepoprawnymi danymi. Znaczenie pytań 2 i 3 w pytaniu ... –

+0

Kontrole danych są wykonywane w usłudze i użytkownik rzuca własny wyjątek. Nie musisz analizować wyjątku SQLException. Izolacja transakcji gwarantuje, że po dokonaniu sprawdzenia w usłudze są one nadal ważne podczas wyciągu "twórz". Nawet jeśli dwie prośby pojawią się w tym samym czasie, jedna z nich zostanie opóźniona, dopóki druga nie zostanie zatwierdzona (lub coś w tym stylu, zależy to od poziomu). Myślę, że obejmuje to pytanie 2 i 3. – pcjuzer

+0

To nie jest takie proste ... Izolacja SERIALIZABLE jest zbyt restrykcyjna, aby można ją było użyć w każdym przypadku, ale nawet REPEATABLE_READ to za mało. Musiałem użyć SELECT DLA AKTUALIZACJI, aby jakoś zadziałało, ale w ten sposób współdziałać będzie cierpieć ... BTW, umieściłem projekt testujący na githubie, aby każdy mógł z nim grać ... https://github.com/ Fantastyczne/gs-managed-transfery –

1

Zgodnie z moim rozumieniem, najlepszym sposobem na obsłużenie transakcji na poziomie DB, musisz efektywnie korzystać z trnsaction Spring Isolation w warstwie dao. Poniżej jest przemysł próbka wzorcowa codde w twoim przypadku ...

public interface OrganizationService { 
    @Retryable(maxAttempts=3,value=DataAccessResourceFailureException.class,[email protected](delay = 1000)) 
    public boolean checkOrganizationWithNameExists(String name);  
} 

@Repository 
@EnableRetry 
public class OrganizationServiceImpl implements OrganizationService { 
    @Transactional(isolation = Isolation.READ_COMMITTED) 
    @Override 
    public boolean checkOrganizationWithNameExists(String name){ 
     //your code 
     return true;  
    } 
} 

Proszę szczypać mnie jeśli się mylę tutaj

0

Rozdzielenie dotyczą:

  • Jax-RS zasobu (punkt końcowy) warstwa: po prostu obsłuż żądanie, wywołaj usługę i zawiń potencjalny wyjątek w odpowiednim kodzie odpowiedzi (po prostu złap i zawiń ręcznie lub użyj exception mapper).
  • Warstwa usługi/biznesu: ujawnia metodę transakcyjną dla każdej jednostki pracy, błąd biznesowy musi być traktowany jako wyjątek zaznaczony, operujący jako niezaznaczony (podklasy RuntimeException).
  • Warstwa dostępu do danych: wystarczy obsłużyć elementy dostępu do danych (tj.pobierz kontekst db, uruchom zapytanie i ewentualnie odwzoruj wynik).

Podkreślam jedną rzecz: dobre miejsce na granice transakcji to miejsce, w którym zdefiniowane są metody biznesowe. Zakres transakcji musi być jednostką biznesową.

Jeśli chodzi o kwestię współbieżności, istnieje 2 sposoby radzenia sobie z tego rodzaju problemem współbieżności: pesymistyczne lub optymistyczne blokowanie.

  • Pesymistyczny:

    • Blokada
    • robić swoje rzeczy
    • Aktualizacja
    • blokada Release
  • optymistyczne

    • wersja check
    • robić swoje rzeczy
    • aktualizację jeśli wersja jest taka sama, nie inaczej

Pesymistyczny jest kwestią dotyczącą skalowalności i wydajności, optymistyczne problemem jest to, że czasami kończy wysyłając błąd operacyjny dla użytkownika końcowego.

bym osobiście pójść z blokowania optymistycznego w Twoim przypadku JOOQ support it

+0

Dla optymistycznego blokowania, Nie jest jasne, jakie rekordy powinna mieć wersja? Chcę chronić operację INSERT przed tworzeniem duplikatów. Ale w takim razie nie zmieniam żadnych rekordów, prawda? Chyba mogę sprawić, żeby to jakoś zadziałało ... jak utwórz tabelę zawierającą wszystkie nazwiska kiedykolwiek używane - ale wydaje się brzydkie ... Jeśli chodzi o pesymistyczną blokadę - masz na myśli, że powinienem użyć 'SELECT..FOR UPDATE'? Lub po prostu użyć izolacji "REPEATABLE READ"? –

+0

Po prostu nie mogę zrozumieć, jeśli faktycznie ochroni to moją sprawę, gdzie WSTAWIĆ nowe zapisy jako decyzję podjętą przez wybranie innych zapisów, tj. sprawdź, czy istnieje nazwa, a następnie wstaw nowy rekord o tej nazwie, w przeciwnym razie nie rób nic. –

+0

Sry czytam zbyt szybko. 'REPEATABLE READ' nie wyklucza wstawiania nowych danych, musisz zablokować całą tabelę i tak ustawić poziom do serializacji. Przypuszczam, że 'wybierz aktualizację' blokuje tylko wybrane wyrażenie i nie jest odpowiednie. Możesz zachować wpis w wersji org tabeli w dedykowanej tabeli, aby zaimplementować optymistyczne blokowanie i wstawić nową wersję org i increment w tej samej transakcji, ale nie jest to zbyt elegancka – Gab