Co tak na prawdę robi @Transactional?

Wróćmy do problemu z poprzedniego wpisu:

Obrazek posiada pusty atrybut alt; plik o nazwie image-3.png
Czemu adnotacja @Transactional w tym przypadku nie ma sensu?

Na początku przypomnijmy szybko 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, że 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, że 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, że 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, że 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. Jeżeli 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.

Obrazek posiada pusty atrybut alt; plik o nazwie image-3.png
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, że pomiędzy tymi dwoma metodami nie ma żadnego proxy*. Brakuje tutaj tej warstwy, która stwierdzi: O! Tutaj jest adnotacja @Transactional, a to oznacza że 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.