2015-01-30 22 views
9

Należy wziąć pod uwagę następującą usługę (domyślnie transakcyjna). Gracz musi zawsze mieć jedno konto. Gracz bez co najmniej jednego odpowiadającego konta jest stanem błędu.Grails 2.4.4: Jak niezawodnie wycofywać w złożonej metodzie serwisowej.

class playerService { 
    def createPlayer() { 
     Player p new Player(name: "Stephen King") 
     if (!p.save()) { 
     return [code: -1, errors:p.errors] 
     } 
     Account a = new Account(type: "cash") 
     if (!a.save()) { 
     // rollback p ! 
     return [code: -2, errors:a.errors] 
     } 
     // commit only now! 
     return [code: 0, player:p] 
    } 
} 

Widziałem tego wzorca przez doświadczonych Grails programistów, a kiedy mówię im, że jeśli powstanie na konto gracza nie z jakiegokolwiek powodu, to przyzwyczajenie wycofać odtwarzacz, a pozostawi DB w nieprawidłowy stan, patrzą na mnie jak na szaleńca, ponieważ grails radzi sobie z wycofywaniem gracza, ponieważ usługi są transakcyjne, prawda?

Tak więc, będąc facetem SQL, szukam sposobu na wywołanie wycofania w grails. Nie ma ani jednego. Według różnych postów, istnieją tylko 2 sposoby wymuszenia grails na wycofanie w usłudze:

  1. rzuca niesprawdzony wyjątek. Wiesz co to jest prawda?
  2. nie używać metod transakcyjnych usług lub adnotacji, użyj tego konstruktu:

.

DomainObject.withTransaction {status -> 
    //stuff 
    if (someError) { 
     status.setRollbackOnly() 
    } 
} 

1. wygeneruje niekontrolowany wyjątek


1,1 Więc musimy rzucać wyjątków czasu wykonywania cofnąć. Jest to dla mnie ok (lubię wyjątki), ale to nie żelu z deweloperami grails, którzy mamy, którzy przeglądają wyjątki jako powrót do Java i jest uncool. Oznacza to również, że musimy zmienić sposób, w jaki aplikacja obecnie korzysta z warstwy usługi.

1.2 Jeśli zostanie zgłoszony wyjątek, utracisz wartość p.errors - utracisz szczegóły sprawdzania poprawności.

1.3 Nasi nowi twórcy grails nie znają różnicy między niesprawdzonym a sprawdzonym wyjątkiem i nie wiedzą, jak odróżnić. To naprawdę niebezpieczne.

1.4. use .save (failOnError: true) Jestem wielkim fanem używania tego, ale nie jest on odpowiedni wszędzie. Czasami trzeba sprawdzić przyczynę przed pójściem dalej, nie wyrzucać wyjątku. Czy wyjątki, które generuje, zawsze są sprawdzane, zawsze niezaznaczone, czy też? To znaczy. czy nastąpi powrót do AWARII failOnError, bez względu na przyczynę? Nikt, o kogo nie pytałem, nie zna odpowiedzi na to, co jest niepokojące, oni używają ślepej wiary, aby uniknąć zepsutych/niespójnych DB.

1.5 Co dzieje się, gdy kontroler wywołuje usługę A, która wywołuje usługę B, a następnie usługę C. Usługa A musi wychwycić każdy wyjątek i zwrócić ładnie sformatowaną wartość zwracaną do kontrolera. Jeśli usługa C zgłasza wyjątek, który jest przechwytywany przez usługę A, usługa wycofuje transakcje Bs? Jest to bardzo ważne, aby wiedzieć, jak zbudować działającą aplikację.

UPDATE 1: Po wykonaniu niektórych testów wydaje się, że każdy wyjątek środowiska wykonawczego, nawet jeśli zostanie zgłoszony i przechwycony w niektórych niepowiązanych wywołaniach podrzędnych, spowoduje, że wszystko w obiekcie nadrzędnym zostanie wycofane. Jednak w sesji nadrzędnej nie jest łatwo zauważyć, że doszło do tego wycofania - musisz upewnić się, że jeśli zauważysz wyjątek, albo ponownie zgłosisz, albo zwrócisz uwagę rozmówcy, aby pokazać, że nie powiodło się w takiej sytuacji. sposób, w jaki wszystko inne zostanie wycofane.

2.withTransaction


2.1 Wydaje się, że jest to konstrukcja bazarowa. Jak mogę to nazwać i co mam przekazać dla parametru "status"? Co to jest "setRollbackOnly" dokładnie. Dlaczego nie nazywa się to tylko "wycofaniem". Co to jest "tylko" część? Jest związany z obiektem domeny, gdy twoja metoda może chcieć zaktualizować kilka różnych obiektów domeny.

2.2 Gdzie należy umieścić ten kod? W klasie DomainObject? W folderze źródłowym (tzn. Nie w usłudze lub kontrolerze?)? Bezpośrednio w kontrolerze? (nie chcemy duplikować logiki biznesowej w kontrolerach)

3. Idealna sytuacja.


3,1 ogólnym przypadku jest chcemy wszystko, co robimy w metodzie usług cofnąć, jeśli coś w tej metodzie usług mogę być zapisywane z dowolnego powodu, albo rzuca żadnego wyjątku dla jakiejkolwiek przyczyny (zaznaczone lub odznaczone).

3.2 Idealnie chciałbym, aby metody serwisowe "zawsze wycofywały się, chyba że wyraźnie wezwę commit", co jest najbezpieczniejszą strategią, ale nie jest to możliwe, wierzę.

Pytanie brzmi, jak osiągnąć idealną sytuację?

Czy wywołanie zapisu (failOnError: true) ZAWSZE wycofuje wszystko, niezależnie od przyczyny niepowodzenia? Nie jest to doskonałe, ponieważ osoba dzwoniąca nie jest łatwo wiedzieć, który obiekt domeny spowodował problem.

A może ludzie definiują wiele klas wyjątków, które podklasy runtimeException, a następnie jawnie łapią każdy z nich w kontrolerze, aby utworzyć odpowiednią odpowiedź? To jest stara metoda Java, a nasze groovy deweloperzy są pooh takie podejście ze względu na ilość kodu płyty kotła będziemy musieli napisać.

Jakie metody wykorzystują ludzie, aby to osiągnąć?

+0

jest to pytanie dla mnie, postanowiłem nadrobić wszystko w kontrolerze i rzucać własne błędy, które są dziećmi RuntimeException. Widzę, że to jedyny sposób na stworzenie właściwej odpowiedzi. –

Odpowiedz

3

Nie nazwałbym się ekspertem, a to pytanie ma ponad rok, ale mogę odpowiedzieć na niektóre z tych pytań, choćby tylko dla przyszłych poszukiwaczy. Właśnie refaktoryzuję niektórych kontrolerów, aby korzystali z usług w celu skorzystania z transakcji.

I have seen this pattern by experienced grails developers, and when I tell them that if creation of the account of the player fails for any reason, it wont rollback the player, and will leave the DB in an invalid state, they look at me like I am mad because grails handles rolling back the player because services are transactional right?

nie widzę w dokumentacji w którym wyraźnie stwierdza, że ​​powrót z metody usługi ma nie wycofywania transakcji, ale nie mogę sobie wyobrazić, że byłoby to bardzo rozsądny zachowanie. Mimo to testowanie to łatwy sposób na wykazanie się.

1.2 If an exception is thrown, you lose the p.errors - you lose the validation detail.

Ponieważ jesteś jedynym rzucającym wyjątek, możesz zrzucić błędy wraz z nim. Na przykład:

// in service 
    if (!email.save()) { 
     throw new ValidationException("Couldn't save email ${params.id}", email.errors) 
    } 

Podczas połowu wyjątek, ponownie załadować instancję (bo rzuca wyjątek czyści sesji), umieścić błędy powrotem do instancji, a następnie przekazać to do widzenia, jak zwykle:

// in controller 
    } catch (ValidationException e) { 
     def email = Email.read(id) 
     email.errors = e.errors 
     render view: "edit", model: [emailInstance: email] 
    } 

Zostało to omówione pod nagłówkiem "Błędy sprawdzania poprawności i wycofywanie zmian" w dół strony od http://grails.github.io/grails-doc/2.4.4/guide/single.html#transactionsRollbackAndTheSession.

1.4. use .save(failOnError: true) I am a big fan of using this, but its not appropriate everywhere. Sometimes you need to check the reason before going further, not throw an exception. Are the exceptions it can generate always checked, always unchecked, or either? I.e. will failOnError AWLAYS rollback, no matter what the cause? No one I have asked knows the answer to this, which is disturbing, they are using blind faith to avoid corrupted/inconsistent DBs.

failOnError will cause save() to throw a ValidationException, więc tak, jeśli jesteś w transakcji i nie sprawdzają, czy wyjątek, transakcja zostanie wycofana.

Ogólnie rzecz biorąc, wydaje się, że nie- "Grailsy" często używają numeru failOnError, prawdopodobnie z powodów wymienionych na liście (np. Braku kontroli). Zamiast tego sprawdza się, czy nie powiodło się niepowodzenie() i podejmuje w związku z tym działanie.

  1. withTransaction

Nie jestem pewien co do tego, ponieważ SpringSource naprawdę zachęca do korzystania z usług we wszystkim. Osobiście też tego nie lubię.

Jeśli chcesz, aby konkretna usługa była nietransakcyjna, a następnie możesz wykonać jedną z jej metod transakcyjnych, możesz po prostu opisać tę jedną metodę za pomocą @Transactional (chyba że twoi programiści nie lubią adnotacji, ponieważ są zbyt "Java";)).

Uwaga! Natychmiast po zaznaczeniu pojedynczej metody za pomocą @Transactional ogólna usługa stanie się nietransakcyjna.

3.1 The general case is we want every thing we do in a service method to roll back if anything in that service method cant be saved for any reason, or throws any exception for any reason (checked or unchecked).

Czuję sprawdzanych wyjątkami nie są ogólnie uważane za „Groovy” (co również sprawia nich nie Grails-Y). Nie jestem pewien co z tego powodu.

Wygląda jednak na to, że możesz poinformować usługę o przywróceniu zaznaczonych wyjątków, wymieniając je w numerze rollbackFor option to @Transactional.

Or do people define lots of exception classes which subclass runtimeException, then explicit catch each of them in the controller to create the appropriate response? This is the old Java way, and our groovy devs pooh pooh this approach due to the amount of boiler plate code we will have to write.

Zaletą Groovy jest to, że można napisać płytę kotła raz, a potem nazywają go wielokrotnie. Wzór Widziałem dużo, a obecnie używam, jest mniej więcej tak:

private void validate(Long id, Closure closure) { 
    try { 
     closure() 
    } catch (ValidationException e) { 
     def email = Email.read(id) 
     email.errors = e.errors 
     render view: "edit", model: [emailInstance: email] 
    } catch (OtherException e) { 
     def email = Email.read(id) 
     flash.error = "${e.message}: ${e.reasons}" 
     render view: "show", model: [emailInstance: email] 
    } catch (Throwable t) { 
     flash.error = "Unexpected error $t: ${t.message}" 
     redirect action: "list" 
    } 
} 

a następnie wywołać go w każdej akcji kontrolera tak:

def update(Long id, Long version) { 
    withInstance(id, version) { Email emailInstance -> 
     validate(emailInstance.id) { 
      emailService.update(emailInstance, params) 
      flash.message = "Email $id updated at ${new Date()}." 
      redirect action: "show", id: emailInstance.id 
     } 
    } 
} 

(withInstance inna podobna metoda które OSUSZą do sprawdzenia istnienia i optymistycznego blokowania.)

To podejście ma wady. W każdym działaniu otrzymuje się ten sam zestaw przekierowań; prawdopodobnie chcesz napisać jeden zestaw metod dla każdego kontrolera; i wydaje się dość głupio przekazać zamknięcie do metody i oczekiwać, że metoda będzie wiedzieć, jakie wyjątki zamknie zamknięcie. Ale hej, programowanie polega na kompromisach, prawda?

W każdym razie, nadzieja jest co najmniej interesująca.

+0

Dużo pomógł, ale jestem sfrustrowany podstawą niezbędną do wdrożenia wszelkich walidacji biznesowych (które nie pasują do walidacji GORM). –

+0

Należy zauważyć, że utknąłem w wersji 2.2.0, więc niektóre z nich mogą być niepotrzebne w nowszych wersjach Grails. –

+0

Korzystanie z laata 2.X i nadal jest konieczne –

0

Jeśli masz usługę taką jak: W aplikacji Grails 2, zalecanym sposobem byłoby użycie transactionStatus.setRollbackOnly().

import grails.transaction.Transactional 

Class RoleService { 

    @Transactional 
    Role save(String authority) { 
     Role roleInstance = new Role(authority: authority) 
     if (!roleInstance.save()) { 
      // log errors here 
      transactionStatus.setRollbackOnly() 
     } 
     roleInstance 
    } 

} 

Patrz: https://github.com/grails/grails-core/issues/9212