All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.phloc.db.jpa.JPAEnabledManager Maven / Gradle / Ivy

/**
 * Copyright (C) 2006-2014 phloc systems
 * http://www.phloc.com
 * office[at]phloc[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.phloc.db.jpa;

import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
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.phloc.commons.callback.AdapterRunnableToCallable;
import com.phloc.commons.callback.AdapterThrowingRunnableToCallable;
import com.phloc.commons.callback.IExceptionHandler;
import com.phloc.commons.callback.IThrowingRunnable;
import com.phloc.commons.stats.IStatisticsHandlerCounter;
import com.phloc.commons.stats.IStatisticsHandlerTimer;
import com.phloc.commons.stats.StatisticsManager;
import com.phloc.commons.timing.StopWatch;
import com.phloc.db.jpa.callback.IExecutionTimeExceededHandler;
import com.phloc.db.jpa.callback.LoggingExecutionTimeExceededHandler;

/**
 * JPA enabled manager with transaction handling etc. The
 * {@link IEntityManagerProvider} 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 IStatisticsHandlerCounter s_aStatsCounterTransactions = StatisticsManager.getCounterHandler (JPAEnabledManager.class.getName () +
                                                                                                                    "$transactions");
  private static final IStatisticsHandlerCounter s_aStatsCounterRollback = StatisticsManager.getCounterHandler (JPAEnabledManager.class.getName () +
                                                                                                                "$rollback");
  private static final IStatisticsHandlerCounter s_aStatsCounterSuccess = StatisticsManager.getCounterHandler (JPAEnabledManager.class.getName () +
                                                                                                               "$success");
  private static final IStatisticsHandlerCounter s_aStatsCounterError = StatisticsManager.getCounterHandler (JPAEnabledManager.class.getName () +
                                                                                                             "$error");
  private static final IStatisticsHandlerTimer s_aStatsTimerExecutionSuccess = StatisticsManager.getTimerHandler (JPAEnabledManager.class.getName () +
                                                                                                                  "$execSuccess");
  private static final IStatisticsHandlerTimer s_aStatsTimerExecutionError = StatisticsManager.getTimerHandler (JPAEnabledManager.class.getName () +
                                                                                                                "$execError");

  protected static final ReadWriteLock s_aRWLock = new ReentrantReadWriteLock ();
  private static IExceptionHandler  s_aExceptionHandler;
  private static final AtomicInteger s_aExecutionWarnTime = new AtomicInteger (DEFAULT_EXECUTION_WARN_TIME_MS);
  private static IExecutionTimeExceededHandler s_aExecutionTimeExceededHandler = new LoggingExecutionTimeExceededHandler (true);

  private final IEntityManagerProvider 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 IEntityManagerProvider aEntityManagerProvider)
  {
    if (aEntityManagerProvider == null)
      throw new NullPointerException ("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 ();
  }

  /**
   * Set a custom exception handler that is called in case performing some
   * operation fails.
   * 
   * @param aExceptionHandler
   *        The exception handler to be set. May be null to
   *        indicate no custom exception handler.
   */
  public static final void setCustomExceptionHandler (@Nullable final IExceptionHandler  aExceptionHandler)
  {
    s_aRWLock.writeLock ().lock ();
    try
    {
      s_aExceptionHandler = aExceptionHandler;
    }
    finally
    {
      s_aRWLock.writeLock ().unlock ();
    }
  }

  /**
   * Get the custom exception handler.
   * 
   * @return null if non is set
   */
  @Nullable
  public static final IExceptionHandler  getCustomExceptionHandler ()
  {
    s_aRWLock.readLock ().lock ();
    try
    {
      return s_aExceptionHandler;
    }
    finally
    {
      s_aRWLock.readLock ().unlock ();
    }
  }

  /**
   * Invoke the custom exception handler (if present)
   * 
   * @param t
   *        The exception that occurred.
   */
  private static void _invokeCustomExceptionHandler (@Nonnull final Throwable t)
  {
    final IExceptionHandler  aExceptionHandler = getCustomExceptionHandler ();
    if (aExceptionHandler == null)
    {
      // By default: log something :)
      s_aLogger.error ("Failed to perform something in a JPAEnabledManager!", t);
    }
    else
      try
      {
        aExceptionHandler.onException (t);
      }
      catch (final Throwable t2)
      {
        s_aLogger.error ("Error in JPAEnabledManager custom exception handler " + aExceptionHandler, t2);
      }
  }

  /**
   * @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)
  {
    if (nMillis < 0)
      throw new IllegalArgumentException ("Milliseconds may not be negative: " + nMillis);
    s_aExecutionWarnTime.set (nMillis);
  }

  public static final void setExecutionTimeExceededHandler (@Nullable final IExecutionTimeExceededHandler aExecutionTimeExceededHandler)
  {
    s_aRWLock.writeLock ().lock ();
    try
    {
      s_aExecutionTimeExceededHandler = aExecutionTimeExceededHandler;
    }
    finally
    {
      s_aRWLock.writeLock ().unlock ();
    }
  }

  /**
   * Get the custom exception handler.
   * 
   * @return null if non is set
   */
  @Nullable
  public static final IExecutionTimeExceededHandler getExecutionTimeExceededHandler ()
  {
    s_aRWLock.readLock ().lock ();
    try
    {
      return s_aExecutionTimeExceededHandler;
    }
    finally
    {
      s_aRWLock.readLock ().unlock ();
    }
  }

  public static final void onExecutionTimeExceeded (@Nonnull final String sMsg, @Nonnegative final long nExecutionMillis)
  {
    final IExecutionTimeExceededHandler aHdl = getExecutionTimeExceededHandler ();
    if (aHdl != null)
      try
      {
        // Invoke the handler
        aHdl.onExecutionTimeExceeded (sMsg, nExecutionMillis);
      }
      catch (final Throwable t)
      {
        s_aLogger.error ("Failed to invoke exceution time exceeded handler " + aHdl, t);
      }
  }

  @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)
  {
    // TODO use AdapterRunnableToThrowingRunnable in phloc-commons > 4.0.5
    return doInTransaction (new IThrowingRunnable ()
    {
      public void run () throws Exception
      {
        aRunnable.run ();
      }
    });
  }

  @Nonnull
  public static final  JPAExecutionResult  doInTransaction (@Nonnull @WillNotClose final EntityManager aEntityMgr,
                                                                  final boolean bAllowNestedTransactions,
                                                                  @Nonnull final Callable  aCallable)
  {
    final StopWatch aSW = new StopWatch (true);
    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 ());
      _invokeCustomExceptionHandler (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
   */
  @Nonnull
  public static final  JPAExecutionResult  doSelectStatic (@Nonnull final Callable  aCallable)
  {
    if (aCallable == null)
      throw new NullPointerException ("callable");

    final StopWatch aSW = new StopWatch (true);
    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 ());
      _invokeCustomExceptionHandler (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.
   */
  @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 - 2024 Weber Informatics LLC | Privacy Policy