Co tak na prawdę robi @Transactional?

pater.dev 5 lat temu

Wróćmy do problemu z poprzedniego wpisu:

Co się dzieje gdy używasz adnotacji @Inject/@Autowired?
Czemu adnotacja @Transactional w tym przypadku nie ma sensu?

Na początku przypomnijmy gwałtownie co robiła adnotacja @Transactional – mocno uproszczając: otwierała nam transakcję przed wywołaniem metody oznaczonej tą adnotacją i zamykała transakcje po zakończeniu działania metody. Może prościej zobrazuje to poniższy fragment kodu.

Zapis do bazy bez wykorzystania adnotacji:

public void save(User user) throws SQLException{ entityManager.getTransaction().begin(); try { return entityManager.persist(user); } finally { entityManager.getTransaction().commit(); entityManager.close(); } }

Oraz po wykorzystaniu adnotacji:

@Transactional public void save(User user) { return entityManager.save(user); }

Widzimy przy okazji jak bardzo oszczędza nam to pisanie powtarzalnego kodu.

Spis treści

  1. Pierwsza myśl – wyciągnijmy powtarzalny kod „wyżej”
  2. ThreadLocal – czyli po co jest nam to w ogóle potrzebne?
  3. Dodajemy adnotację @Transactional
  4. Odpowiedź na pytanie – czemu adnotacja @Transactional w tym przypadku nie ma sensu?

Wyciągnijmy powtarzalny kod

Dosyć normalna reakcja – widzimy powtarzalny kod i zaświeca nam się lampka, iż coś trzeba z nim zrobić.

public void save(User user) throws SQLException{ entityManager.getTransaction().begin(); try { return entityManager.persist(user); } finally { entityManager.getTransaction().commit(); entityManager.close(); } }

Tak na prawdę wszystko oprócz entityManager.persist(user); powtarza się za każdym razem. Wyciągnijmy to więc do osobnej metody wykorzystując coś na kształt metody szablonowej.

public interface TransactionTemplate { <T> T execute(TransactionalOperation<T> action) throws SQLException; } public class TransactionTemplateImpl implements TransactionTemplate { @Override public <T> T execute(Function<T> action) throws SQLException { // pobierz wcześniej w jakiś sposób entityManagera // [...] entityManager.getTransaction().begin(); try { // dokładnie nasze entityManager.persist(..); return action.run(); } finally { entityManager.getTransaction().commit(); entityManager.close(); } } }

I widzimy, iż zrealizowaliśmy jakiś tam mały cel – redukcji powtarzającego się kodu. No prawie… Zostaje nam jeszcze kwestia tego skąd wziąć tego EntityManagera.

ThreadLocal – czyli po co jest nam to w ogóle potrzebne?

Pierwsza myśl jaka nam przychodzi – zawołajmy o niego bezpośrednio z EntityManagerFactory. O ile utworzenie fabryki jest bardzo zasobożerne tak zawołanie po samego EntityManagera jest już stosunkowo tanie.

Tyle, iż zawsze jest jakieś ale. Taka praktyka (EntityManager per operacja) jest po prostu zła z kilku powodów:

First, don’t use the entitymanager-per-operation antipattern, that is, don’t open and close an EntityManagerfor every simple database call in a single thread! Of course, the same is true for database transactions. Database calls in an application are made using a planned sequence, they are grouped into atomic units of work. (Note that this also means that auto-commit after every single SQL statement is useless in an application, this mode is intended for ad-hoc SQL console work.)

https://docs.jboss.org/hibernate/core/4.0/hem/en-US/html/transactions.html

Najczęściej wykorzystywaną praktyką w aplikacjach client/server jest EntityManager per request. Oznacza to, iż za każdym zapytaniem do serwera otwierany jest nowy EntityManager. I tutaj z pomocą przychodzi nam konstrukcja ThreadLocal, która pozwala nam na przechowywanie danych per wątek. W większości serwerów każdy request jest osobnym wątkiem. Więcej o tym można poczytać How do servlets handle multiple requests?

Each request is processed in a separated thread. This doesn’t mean tomcat creates a thread per request. There a is pool of threads to process requests.

https://www.quora.com/How-do-servlets-handle-multiple-requests

Skoro już wiemy skąd będziemy pobierać EntityManager to możemy przejść do kodu.

Prosta klasa do przechowywania naszego EntityManagera:

public class EMThreadLocalStorage { private static final ThreadLocal<EntityManager> threadLocal = new ThreadLocal<>(); public static EntityManager getEntityManager() { return threadLocal.get(); } public static void setEntityManager(EntityManager entityManager) { threadLocal.set(entityManager); } }

I dodatkowo zaktualizujmy wcześniejszą metodę o brakujący fragment:

public class TransactionTemplate implements TransactionTemplate { @Override public <T> T execute(Function<T> action) throws SQLException { EntityManager entityManager = EMThreadLocalStorage.getEntityManager(); entityManager.getTransaction().begin(); try { // dokładnie nasze entityManager.persist(..); return action.run(); } finally { entityManager.getTransaction().commit(); entityManager.close(); } } }

Dodajemy adnotację @Transactional

Przejdźmy do kolejnego kroku. Chcemy, aby nasza metoda była wykonywana w TransactionTemplate w momencie gdy jest oznaczona adnotacją @Transactional. Zaczniemy od utworzenia samej adnotacji:

@Retention(RetentionPolicy.RUNTIME) public @interface Transactional {}

Teraz będę opierał się na kodzie z poprzedniego wpisu. Będziemy do trochę rozbudowywać. Zacznijmy od metody getBean. W tym przypadku dojdzie następująca zmiana. Musimy opakować nasz obiekt w proxy.

public <T> T getBean(Class<T> clazz) { try { T obj = getBeanFromConfiguration(clazz); Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(Inject.class)) { field.setAccessible(true); field.set(obj, getBean(field.getType())); } } if (clazz.isInterface()) { // opakowujemy nasz obiekt w proxy transakcyjności - szablonem jest nasz TransactionTemplateImpl() TransactionalHandler transactionalHandler = new TransactionalHandler(obj, new TransactionTemplateImpl()); obj = (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz }, transactionalHandler); } return obj; } catch (IllegalAccessException | InstantiationException e) { e.printStackTrace(); } return null; }

Opakowujemy nasz obiekt w proxy TransactionHandler, którego zadaniem jest sprawdzenie, przy każdym wywołaniu metody danego obiektu, czy czasem nie jest ona (ta metoda lub klasa) oznaczona adnotacją @Transactional. o ile tak jest to chcemy, aby została ona wywołana w obrębie transakcji – czyli opakowana w naszego TransactionTemplate. W tym celu nasza klasa musi implementować interfejs InvocationHandler. O dynamicznym proxy więcej poczytasz Java dynamic proxies.

public class TransactionalHandler implements InvocationHandler { private Object object; private TransactionTemplate transactionTemplate; public TransactionalHandler(Object object, TransactionTemplate transactionTemplate) { super(); this.transactionTemplate = transactionTemplate; this.object = object; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable, InvocationTargetException, IllegalAccessException, IllegalArgumentException { Class<?> clazz = object.getClass(); boolean runInTransaction = clazz.isAnnotationPresent(Transactional.class) || clazz.getMethod(method.getName(), method.getParameterTypes()).isAnnotationPresent(Transactional.class); // jezeli klasa lub metoda oznaczona @Transactional to uruchom w TransactionTemplate if (runInTransaction) { return transactionTemplate.execute(() -> { try { return method.invoke(object, args); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { e.printStackTrace(); } return null; }); } else { return method.invoke(object, args); } } }

Czemu adnotacja @Transactional w tym przypadku nie ma sensu?

Wróćmy do pytania z samego początku wypisu.

Niepoprawne wykorzystanie adnotacji @Transactional

Czy widzisz już czemu to nie zadziała?

Metoda secondMethod wywoływana jest bezpośrednio z tej samej klasy (metody save). Oznacza to, iż pomiędzy tymi dwoma metodami nie ma żadnego proxy*. Brakuje tutaj tej warstwy, która stwierdzi: O! Tutaj jest adnotacja @Transactional, a to oznacza iż muszę odpalić tę metodę w transakcji.

Przedstawiając to obrazowo:

Stack trace wywołania metody save()
Stack trace wywołania metody save() z proxy pomiędzy

*Mowa tutaj o najpopularniejszej sytuacji – proxy Springa (a on wykorzystuje mechanizm JDK dynamic proxies or CGLIB – do wyboru)tutaj więcej do poczytania o mechanizmie proxy w Springu

Całość kodu jak zwykle na GitHubie.

Idź do oryginalnego materiału