package io.robe.hibernate.transaction; import io.robe.hibernate.RobeHibernateBundle; import org.hibernate.FlushMode; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.context.internal.ManagedSessionContext; import org.hibernate.resource.transaction.spi.TransactionStatus; import java.util.Stack; import static com.google.common.base.Preconditions.checkNotNull; /** * Provides a new {@link Session} and run {@link TransactionWrapper} in this session scope. * So any transaction done here does not affect the transaction of session provided by {@link io.dropwizard.hibernate.UnitOfWork} * and the session that provided by this class in another session scope unless you catch the exceptions. <br/> * <b>Example usages</b> <br/> * <pre> * {@code * * Transaction.exec(() -> { * dao.create(entity); * }); * * Transaction.exec(() -> { * dao.create(entity); * Transaction.exec(() -> { * anotherDao.create(anotherEntity); * }, FlushMode.COMMIT); * }); * * Transaction.exec(() -> { * dao.create(entity); * }, (exception) -> { * logger.log(exception); * throw exception; * }); * } * </pre> */ public class Transaction { private static final SessionFactory SESSION_FACTORY = RobeHibernateBundle.getInstance().getSessionFactory(); private static ThreadLocal<Stack<Session>> SESSIONS = new ThreadLocal<Stack<Session>>() { protected Stack<Session> initialValue() { return new Stack<>(); } }; private Session session; private TransactionWrapper transactionWrapper; private TransactionExceptionHandler exceptionHandler; private FlushMode flushMode; /** * Private constructor * * @param transactionWrapper {@link TransactionWrapper} * @param exceptionHandler {@link TransactionExceptionHandler} * @param flushMode {@link FlushMode} */ private Transaction(TransactionWrapper transactionWrapper, TransactionExceptionHandler exceptionHandler, FlushMode flushMode) { this.transactionWrapper = transactionWrapper; this.exceptionHandler = exceptionHandler; this.flushMode = flushMode; storePreviousSession(); configureNewSession(); start(); } /** * Begins new transaction in a new session and performs operations provided in {@link TransactionWrapper} * * @param transactionWrapper {@link TransactionWrapper} */ public static void exec(TransactionWrapper transactionWrapper) { exec(transactionWrapper, null, FlushMode.AUTO); } /** * Begins new transaction in a new session and performs operations provided in {@link TransactionWrapper} <br/> * In case of an exception is thrown the {@link TransactionExceptionHandler} will be invoked. * * @param transactionWrapper {@link TransactionWrapper} * @param exceptionHandler {@link TransactionExceptionHandler} */ public static void exec(TransactionWrapper transactionWrapper, TransactionExceptionHandler exceptionHandler) { exec(transactionWrapper, exceptionHandler, FlushMode.AUTO); } /** * Begins new transaction in a new session and performs operations provided in {@link TransactionWrapper} * * @param transactionWrapper {@link TransactionWrapper} * @param flushMode {@link FlushMode} */ public static void exec(TransactionWrapper transactionWrapper, FlushMode flushMode) { exec(transactionWrapper, null, flushMode); } /** * Begins new transaction in a new session and performs operations provided in {@link TransactionWrapper} <br/> * In case of an exception is thrown the {@link TransactionExceptionHandler} will be invoked. * * @param transactionWrapper {@link TransactionWrapper} * @param exceptionHandler {@link TransactionExceptionHandler} * @param flushMode {@link FlushMode} */ public static void exec(TransactionWrapper transactionWrapper, TransactionExceptionHandler exceptionHandler, FlushMode flushMode) { checkNotNull(transactionWrapper); new Transaction(transactionWrapper, exceptionHandler, flushMode); } /** * If present, backup current the session in {@link ManagedSessionContext} and unbinds it. */ private void storePreviousSession() { if (ManagedSessionContext.hasBind(SESSION_FACTORY)) { SESSIONS.get().add(SESSION_FACTORY.getCurrentSession()); ManagedSessionContext.unbind(SESSION_FACTORY); } } /** * Opens a new session, sets flush mode and bind this session to {@link ManagedSessionContext} */ private void configureNewSession() { session = SESSION_FACTORY.openSession(); session.setFlushMode(flushMode); ManagedSessionContext.bind(session); } /** * Start transaction */ private void before() { session.beginTransaction(); } /** * If transaction is present and active then commit. */ private void success() { org.hibernate.Transaction txn = session.getTransaction(); if (txn != null && txn.getStatus().equals(TransactionStatus.ACTIVE)) { txn.commit(); } } /** * If transaction is present and active then rollback. */ private void error() { org.hibernate.Transaction txn = session.getTransaction(); if (txn != null && txn.getStatus().equals(TransactionStatus.ACTIVE)) { txn.rollback(); } } /** * Closes and unbinds the current session. <br/> * If {@link ManagedSessionContext} had a session before the current session, re-binds it to {@link ManagedSessionContext} */ private void finish() { try { if (session != null) { session.close(); } } finally { ManagedSessionContext.unbind(SESSION_FACTORY); if (!SESSIONS.get().isEmpty()) { ManagedSessionContext.bind(SESSIONS.get().pop()); } } } /** * Starts the progress. */ private void start() { try { before(); transactionWrapper.wrap(); success(); } catch (Exception e) { error(); if (exceptionHandler != null) { exceptionHandler.onException(e); } else { throw e; } } finally { finish(); } } }