package com.maxifier.guice.jpa;
import com.maxifier.guice.jpa.DB.Transaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
/**
* Class used for database context handling.
* <p>Class uses thread-local state to handle database context propagation.</p>
* <p>Database context can be handled manually. Use manual handling with caution, mismatch between
* {@code begin()} and {@code end()} calls cause huge memory leaks.</p>
* <p>Usage:</p>
* <pre>
* class Worker {
* @Inject EntityManager em;
*
* void doWork() {
* UnitOfWork.begin();
* try {
* em.find(Foo.class, 7);
* } finally {
* UnitOfWork.end();
* }
* }
* }
* </pre>
* <p>Preferred for use in tests. Use {@link DBEntityManagerProvider} to create proper EM.</p>
*
* @author Konstantin Lyamshin (2015-11-09 18:22)
*/
public final class UnitOfWork {
private static final Logger logger = LoggerFactory.getLogger(UnitOfWork.class);
private static final ThreadLocal<UnitOfWork> current = new ThreadLocal<UnitOfWork>();
private final UnitOfWork previous;
private EntityManager entityManager;
private boolean startTransaction;
/**
* Starts a new UnitOfWork for the current thread.
* <p>UnitOfWork holds actual database context.</p>
* <p>Always creates new UnitOfWork like in {@link Transaction#REQUIRES_NEW} mode. But don't start transaction
* like in {@link Transaction#NOT_REQUIRED} mode. All transactions should be handled manually.</p>
*/
public static void begin() {
create();
}
/**
* Finishes current UnitOfWork.
* <p>Current UnitOfWork must be previously started by {@link #begin()}.</p>
*/
public static void end() {
UnitOfWork context = current.get();
if (context != null) {
context.releaseConnection();
} else {
logger.error("Corrupted UnitOfWork call stack", new IllegalStateException());
}
}
@Nullable
static UnitOfWork get() {
return current.get();
}
@Nonnull
static UnitOfWork create() {
UnitOfWork context = new UnitOfWork(current.get());
current.set(context);
return context;
}
private UnitOfWork(UnitOfWork previous) {
this.previous = previous;
}
boolean startTransaction() {
if (startTransaction) {
return false;
}
if (entityManager != null) {
entityManager.getTransaction().begin();
}
startTransaction = true;
return true;
}
void endTransaction() {
if (entityManager != null) {
EntityTransaction tr = entityManager.getTransaction();
if (tr.getRollbackOnly()) {
tr.rollback();
} else {
tr.commit();
}
}
startTransaction = false;
}
void setRollbackOnly() {
if (entityManager != null) {
EntityTransaction tr = entityManager.getTransaction();
if (tr.isActive()) {
tr.setRollbackOnly();
}
}
}
EntityManager getConnection(EntityManagerFactory entityManagerFactory) {
if (entityManager == null) {
entityManager = entityManagerFactory.createEntityManager();
if (startTransaction) {
entityManager.getTransaction().begin();
}
} else if (entityManager.getEntityManagerFactory() != entityManagerFactory) {
throw new IllegalStateException("Multiple EntityManager instances not allowed within one DB context");
}
return entityManager;
}
void releaseConnection() {
UnitOfWork context = current.get();
if (context != this) {
logger.error("Corrupted UnitOfWork call stack", new IllegalStateException());
}
// restore stack in-advance
current.set(previous);
if (entityManager == null) {
return; // nothing to release
}
// exception-prone code
EntityTransaction tr = entityManager.getTransaction();
if (tr.isActive()) {
logger.warn("Unfinished transaction found, trying to complete", new IllegalStateException());
if (tr.getRollbackOnly()) {
tr.rollback();
} else {
tr.commit();
}
}
entityManager.close();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
if (entityManager != null) {
sb.append("UnitOfWork{connected").append(startTransaction? ", transactional}": "}");
} else if (startTransaction) {
sb.append("UnitOfWork{transactional}");
} else {
sb.append("UnitOfWork{}");
}
return sb.toString();
}
}