Wróćmy do problemu z poprzedniego wpisu:
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
- Pierwsza myśl – wyciągnijmy powtarzalny kod „wyżej”
- ThreadLocal – czyli po co jest nam to w ogóle potrzebne?
- Dodajemy adnotację @Transactional
- 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.htmlNajczęś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-requestsSkoro 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.
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:
*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.