
com.helger.db.jpa.JPAEnabledManager Maven / Gradle / Ivy
/**
* Copyright (C) 2014-2016 Philip Helger (www.helger.com)
* philip[at]helger[dot]com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.helger.db.jpa;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.WillNotClose;
import javax.annotation.concurrent.ThreadSafe;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.Query;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.ReturnsMutableObject;
import com.helger.commons.callback.CallbackList;
import com.helger.commons.callback.IThrowingRunnable;
import com.helger.commons.callback.adapter.AdapterRunnableToCallable;
import com.helger.commons.callback.adapter.AdapterRunnableToThrowingRunnable;
import com.helger.commons.callback.adapter.AdapterThrowingRunnableToCallable;
import com.helger.commons.callback.exception.IExceptionCallback;
import com.helger.commons.concurrent.SimpleReadWriteLock;
import com.helger.commons.statistics.IMutableStatisticsHandlerCounter;
import com.helger.commons.statistics.IMutableStatisticsHandlerTimer;
import com.helger.commons.statistics.StatisticsManager;
import com.helger.commons.timing.StopWatch;
import com.helger.db.jpa.callback.IExecutionTimeExceededCallback;
import com.helger.db.jpa.callback.LoggingExecutionTimeExceededCallback;
/**
* JPA enabled manager with transaction handling etc. The
* {@link IHasEntityManager} required in the constructor should be a request
* singleton that ensures one {@link EntityManager} per thread. The main
* {@link EntityManager} objects are usually create from a subclass of
* {@link AbstractGlobalEntityManagerFactory}.
*
* @author Philip Helger
*/
@ThreadSafe
public class JPAEnabledManager
{
/** By default the entity manager is not locked (changed in 3.0.0) */
public static final boolean DEFAULT_SYNC_ENTITY_MGR = false;
/** By default nested transactions are not allowed */
public static final boolean DEFAULT_ALLOW_NESTED_TRANSACTIONS = false;
/** By default no transaction is used for select statements */
public static final boolean DEFAULT_USE_TRANSACTIONS_FOR_SELECT = false;
/** The default execution time after which a warning is emitted */
public static final int DEFAULT_EXECUTION_WARN_TIME_MS = 1000;
private static final Logger s_aLogger = LoggerFactory.getLogger (JPAEnabledManager.class);
private static final IMutableStatisticsHandlerCounter s_aStatsCounterTransactions = StatisticsManager.getCounterHandler (JPAEnabledManager.class.getName () +
"$transactions");
private static final IMutableStatisticsHandlerCounter s_aStatsCounterRollback = StatisticsManager.getCounterHandler (JPAEnabledManager.class.getName () +
"$rollback");
private static final IMutableStatisticsHandlerCounter s_aStatsCounterSuccess = StatisticsManager.getCounterHandler (JPAEnabledManager.class.getName () +
"$success");
private static final IMutableStatisticsHandlerCounter s_aStatsCounterError = StatisticsManager.getCounterHandler (JPAEnabledManager.class.getName () +
"$error");
private static final IMutableStatisticsHandlerTimer s_aStatsTimerExecutionSuccess = StatisticsManager.getTimerHandler (JPAEnabledManager.class.getName () +
"$execSuccess");
private static final IMutableStatisticsHandlerTimer s_aStatsTimerExecutionError = StatisticsManager.getTimerHandler (JPAEnabledManager.class.getName () +
"$execError");
protected static final SimpleReadWriteLock s_aRWLock = new SimpleReadWriteLock ();
private static final CallbackList > s_aExceptionCallbacks = new CallbackList <> ();
private static final AtomicInteger s_aExecutionWarnTime = new AtomicInteger (DEFAULT_EXECUTION_WARN_TIME_MS);
private static final CallbackList s_aExecutionTimeExceededHandlers = new CallbackList <> ();
static
{
// Add default handler
s_aExceptionCallbacks.addCallback (x -> s_aLogger.error ("Failed to perform something in a JPAEnabledManager!", x));
s_aExecutionTimeExceededHandlers.addCallback (new LoggingExecutionTimeExceededCallback (true));
}
private final IHasEntityManager m_aEntityManagerProvider;
private final AtomicBoolean m_aSyncEntityMgr = new AtomicBoolean (DEFAULT_SYNC_ENTITY_MGR);
private final AtomicBoolean m_aAllowNestedTransactions = new AtomicBoolean (DEFAULT_ALLOW_NESTED_TRANSACTIONS);
private final AtomicBoolean m_aUseTransactionsForSelect = new AtomicBoolean (DEFAULT_USE_TRANSACTIONS_FOR_SELECT);
public JPAEnabledManager (@Nonnull final IHasEntityManager aEntityManagerProvider)
{
ValueEnforcer.notNull (aEntityManagerProvider, "EntityManagerProvider");
m_aEntityManagerProvider = aEntityManagerProvider;
}
public final boolean isSyncEntityMgr ()
{
return m_aSyncEntityMgr.get ();
}
/**
* Set whether the entity manager should be synchronized upon each access
*
* @param bSyncEntityMgr
* true
to enable sync, false
to disable sync
*/
public final void setSyncEntityMgr (final boolean bSyncEntityMgr)
{
m_aSyncEntityMgr.set (bSyncEntityMgr);
}
public final boolean isAllowNestedTransactions ()
{
return m_aAllowNestedTransactions.get ();
}
/**
* Allow nested transaction
*
* @param bAllowNestedTransactions
* true
to enable nested transaction
*/
public final void setAllowNestedTransactions (final boolean bAllowNestedTransactions)
{
m_aAllowNestedTransactions.set (bAllowNestedTransactions);
}
/**
* @return true
if transactions should be used for selecting,
* false
if this can be done without transactions
*/
public final boolean isUseTransactionsForSelect ()
{
return m_aUseTransactionsForSelect.get ();
}
/**
* Use transactions for select statements?
*
* @param bUseTransactionsForSelect
* true
to enable the usage of transactions for select
* statements.
*/
public final void setUseTransactionsForSelect (final boolean bUseTransactionsForSelect)
{
m_aAllowNestedTransactions.set (bUseTransactionsForSelect);
}
/**
* @return Get the entity manager to be used. Must not be null
.
*/
@Nonnull
protected final EntityManager getEntityManager ()
{
return m_aEntityManagerProvider.getEntityManager ();
}
/**
* Get the custom exception handler list for modification.
*
* @return null
if non is set
*/
@Nonnull
@ReturnsMutableObject ("by design")
public static final CallbackList > getCustomExceptionCallbacks ()
{
return s_aExceptionCallbacks;
}
/**
* Invoke the custom exception handler (if present)
*
* @param t
* The exception that occurred.
*/
private static void _invokeCustomExceptionCallback (@Nonnull final Throwable t)
{
s_aExceptionCallbacks.forEach (x -> x.onException (t));
}
/**
* @return The milliseconds after which a warning is emitted, if an SQL
* statement takes longer to execute.
*/
@Nonnegative
public static final int getDefaultExecutionWarnTime ()
{
return s_aExecutionWarnTime.get ();
}
/**
* Set the milli seconds duration on which a warning should be emitted, if a
* single SQL execution too at least that long.
*
* @param nMillis
* The number of milli seconds. Must be ≥ 0.
*/
public static final void setDefaultExecutionWarnTime (final int nMillis)
{
ValueEnforcer.isGE0 (nMillis, "Milliseconds");
s_aExecutionWarnTime.set (nMillis);
}
/**
* Get the custom exception handler list.
*
* @return Never null
.
*/
@Nonnull
public static final CallbackList getExecutionTimeExceededHandlers ()
{
return s_aExecutionTimeExceededHandlers;
}
public static final void onExecutionTimeExceeded (@Nonnull final String sMsg,
@Nonnegative final long nExecutionMillis)
{
s_aExecutionTimeExceededHandlers.forEach (x -> x.onExecutionTimeExceeded (sMsg, nExecutionMillis));
}
@Nonnull
public static final JPAExecutionResult > doInTransaction (@Nonnull @WillNotClose final EntityManager aEntityMgr,
final boolean bAllowNestedTransactions,
@Nonnull final Runnable aRunnable)
{
return doInTransaction (aEntityMgr, bAllowNestedTransactions, AdapterRunnableToCallable.createAdapter (aRunnable));
}
@Nonnull
public static final JPAExecutionResult > doInTransaction (@Nonnull @WillNotClose final EntityManager aEntityMgr,
final boolean bAllowNestedTransactions,
@Nonnull final IThrowingRunnable aRunnable)
{
return doInTransaction (aEntityMgr,
bAllowNestedTransactions,
AdapterThrowingRunnableToCallable.createAdapter (aRunnable));
}
@Nonnull
public final JPAExecutionResult > doInTransaction (@Nonnull final IThrowingRunnable aRunnable)
{
// Create entity manager
final EntityManager aEntityMgr = getEntityManager ();
if (!isSyncEntityMgr ())
{
// No synchronization required
return doInTransaction (aEntityMgr, isAllowNestedTransactions (), aRunnable);
}
// Sync on the whole entity manager, to have a cross-manager
// synchronization!
synchronized (aEntityMgr)
{
return doInTransaction (aEntityMgr, isAllowNestedTransactions (), aRunnable);
}
}
@Nonnull
public final JPAExecutionResult > doInTransaction (@Nonnull final Runnable aRunnable)
{
return doInTransaction (new AdapterRunnableToThrowingRunnable (aRunnable));
}
@Nonnull
public static final JPAExecutionResult doInTransaction (@Nonnull @WillNotClose final EntityManager aEntityMgr,
final boolean bAllowNestedTransactions,
@Nonnull final Callable aCallable)
{
final StopWatch aSW = StopWatch.createdStarted ();
final EntityTransaction aTransaction = aEntityMgr.getTransaction ();
final boolean bTransactionRequired = !bAllowNestedTransactions || !aTransaction.isActive ();
if (bTransactionRequired)
{
s_aStatsCounterTransactions.increment ();
aTransaction.begin ();
}
try
{
// Execute whatever you want to do
final T ret = aCallable.call ();
// And if no exception was thrown, commit it
if (bTransactionRequired)
aTransaction.commit ();
s_aStatsCounterSuccess.increment ();
s_aStatsTimerExecutionSuccess.addTime (aSW.stopAndGetMillis ());
return JPAExecutionResult.createSuccess (ret);
}
catch (final Throwable t)
{
s_aStatsCounterError.increment ();
s_aStatsTimerExecutionError.addTime (aSW.stopAndGetMillis ());
_invokeCustomExceptionCallback (t);
return JPAExecutionResult.createFailure (t);
}
finally
{
if (bTransactionRequired)
if (aTransaction.isActive ())
{
// We got an exception -> rollback
aTransaction.rollback ();
s_aLogger.warn ("Rolled back transaction for callable " + aCallable);
s_aStatsCounterRollback.increment ();
}
if (aSW.getMillis () > getDefaultExecutionWarnTime ())
onExecutionTimeExceeded ("Callback: " +
aSW.getMillis () +
" ms; transaction: " +
bTransactionRequired +
"; Execution of callable in transaction took too long: " +
aCallable.toString (),
aSW.getMillis ());
}
}
@Nonnull
public final JPAExecutionResult doInTransaction (@Nonnull final Callable aCallable)
{
// Create entity manager
final EntityManager aEntityMgr = getEntityManager ();
if (!isSyncEntityMgr ())
{
// No synchronization required
return doInTransaction (aEntityMgr, isAllowNestedTransactions (), aCallable);
}
// Sync on the whole entity manager, to have a cross-manager
// synchronization!
synchronized (aEntityMgr)
{
return doInTransaction (aEntityMgr, isAllowNestedTransactions (), aCallable);
}
}
/**
* Perform a select, without a transaction
*
* @param aCallable
* The callable
* @return The return of the callable or null
upon success
* @param
* The return type of the callable
*/
@Nonnull
public static final JPAExecutionResult doSelectStatic (@Nonnull final Callable aCallable)
{
ValueEnforcer.notNull (aCallable, "Callable");
final StopWatch aSW = StopWatch.createdStarted ();
try
{
// Call callback
final T ret = aCallable.call ();
s_aStatsCounterSuccess.increment ();
s_aStatsTimerExecutionSuccess.addTime (aSW.stopAndGetMillis ());
return JPAExecutionResult.createSuccess (ret);
}
catch (final Throwable t)
{
s_aStatsCounterError.increment ();
s_aStatsTimerExecutionError.addTime (aSW.stopAndGetMillis ());
_invokeCustomExceptionCallback (t);
return JPAExecutionResult. createFailure (t);
}
finally
{
if (aSW.getMillis () > getDefaultExecutionWarnTime ())
onExecutionTimeExceeded ("Execution of select took too long: " + aCallable.toString (), aSW.getMillis ());
}
}
/**
* Run a read-only query. By default no transaction is used, and the entity
* manager is synchronized.
*
* @param aCallable
* The callable to execute.
* @return A non-null
result of the select.
* @param
* Return type of the callable
*/
@Nonnull
public final JPAExecutionResult doSelect (@Nonnull final Callable aCallable)
{
if (isUseTransactionsForSelect ())
{
// Use transactions for select statement!
return doInTransaction (aCallable);
}
// Ensure that only one transaction is active for all users!
final EntityManager aEntityMgr = getEntityManager ();
if (!isSyncEntityMgr ())
{
// No synchronization required
return doSelectStatic (aCallable);
}
// Sync on the whole entity manager, to have a cross-manager
// synchronization!
synchronized (aEntityMgr)
{
return doSelectStatic (aCallable);
}
}
/**
* Helper method to handle the execution of "SELECT COUNT(...) ..." SQL
* statements. To be invoked inside a {@link #doSelect(Callable)} or
* {@link #doSelectStatic(Callable)} method.
*
* @param aQuery
* The SELECT COUNT query
* @return a non-negative row count
*/
@Nonnull
public static final Number getSelectCountResultObj (@Nonnull final Query aQuery)
{
final Number ret = (Number) aQuery.getSingleResult ();
return ret != null ? ret : Integer.valueOf (0);
}
/**
* Helper method to handle the execution of "SELECT COUNT(...) ..." SQL
* statements. To be invoked inside a {@link #doSelect(Callable)} or
* {@link #doSelectStatic(Callable)} method.
*
* @param aQuery
* The SELECT COUNT query
* @return a non-negative row count
*/
@Nonnegative
public static final long getSelectCountResult (@Nonnull final Query aQuery)
{
return getSelectCountResultObj (aQuery).longValue ();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy