Java tutorial
/*************************************************************************** * Copyright (c) 2012 VMware, Inc. All Rights Reserved. * 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.vmware.bdd.dal; import java.io.Serializable; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Random; import org.apache.log4j.Logger; import org.hibernate.Criteria; import org.hibernate.FlushMode; import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.criterion.Criterion; import org.hibernate.criterion.Order; import org.hibernate.exception.ConstraintViolationException; import org.hibernate.exception.GenericJDBCException; import org.hibernate.exception.LockAcquisitionException; import com.vmware.bdd.entity.Saveable; import com.vmware.bdd.exception.BddException; import com.vmware.bdd.exception.TxRetryException; import com.vmware.bdd.exception.UniqueConstraintViolationException; import com.vmware.bdd.utils.AuAssert; import com.vmware.bdd.utils.ConfigInfo; import com.vmware.bdd.utils.Configuration; /** * This static class serves as the database access layer for the rest * of the code. **/ public class DAL { private static final boolean stressTxnRollback = Configuration.getBoolean("dal.stressTxnRollback", false); private static final Logger logger = Logger.getLogger(DAL.class); private static final SessionFactory sessionFactory = buildSessionFactory(); private static Random rnd = new Random(); private static ThreadLocal<Boolean> inCheckedRegion = new ThreadLocal<Boolean>(); private static final int defaultRetries = 5; private static void checkIsolationLevel(Session _ssn) throws Throwable { Transaction tx = null; try { tx = _ssn.beginTransaction(); tx.commit(); } catch (Throwable exc) { if (tx != null) { tx.rollback(); } throw exc; } } private static SessionFactory buildSessionFactory() { try { SessionFactory _ssn = new org.hibernate.cfg.Configuration().configure().buildSessionFactory(); checkIsolationLevel(_ssn.getCurrentSession()); return _ssn; } catch (Throwable ex) { throw new ExceptionInInitializerError(ex); } } private static Session getSession() { return sessionFactory.getCurrentSession(); } public static boolean isInTransaction() { return getSession().getTransaction().isActive(); } /* * Logs a message when retrying a transaction. * * @param the "real" exception thrown from the transaction body. It can * be null if the body threw BddException with no cause. */ private static void reportRetry(int retriesLeft, Throwable ex) { StackTraceElement[] bt = ex.getStackTrace(); final int bodyFrame = 3; // After autoTransactionDo + findBy if (bt.length > bodyFrame) { // Print a notice but be concise about it. logger.info(ex + ". Retrying " + bt[bodyFrame] + ". " + retriesLeft + " attempts left."); } else { // Print the full backtrace, if it is short by itself. logger.info("Retrying. " + retriesLeft + " attempts left.", ex); } } /** * @param ex -- the "real" exception thrown from the transaction body. It can * be null if we throw BddException with no cause. * @return true if the given exception corresponds to Hibernate's * uniqueness violation. **/ private static boolean isUniqViolation(Throwable ex) { final String uniquenessViolation = new String("23505"); // SQL State error return ex instanceof ConstraintViolationException && ((ConstraintViolationException) ex).getSQLState().equals(uniquenessViolation); } /** * @param ex -- the "real" exception thrown from the transaction body. It can * be null if we throw BddException with no cause. * @return true if the given exception should cause a retry, * e.g. serialization failure or deadlock etc. **/ private static boolean isRetryable(Throwable ex) { final String psqlDeadlockDetected = new String("40P01"); // SQL State error specific to PSQL return ((ex instanceof LockAcquisitionException) || (ex instanceof GenericJDBCException && ((GenericJDBCException) ex).getSQLState().equals(psqlDeadlockDetected))); } /** * Helper routine for wrapping a piece of code in a Hibernate transaction. * * @param obj -- the body of the transaction. * @param readOnly -- true if the writes are to be disallowed * @param retriesLeft -- the max number of times to retry on lock-acquisition exceptions. * 0 if retries are to be disallowed. **/ @SuppressWarnings("deprecation") private static <T> T inTransactionDoWork(Saveable<T> obj, boolean readOnly, int retriesLeft) { T retval; while (true) { Session sn = getSession(); Transaction tx = null; FlushMode flushMode = null; boolean doRndRollback = ConfigInfo.isDebugEnabled() && stressTxnRollback && (rnd.nextInt() % 5) == 0; AuAssert.check(!isInTransaction()); // Disallow nesting for now. try { tx = sn.beginTransaction(); if (readOnly && tx != null) { flushMode = sn.getFlushMode(); sn.setFlushMode(FlushMode.MANUAL); } sn.connection().setReadOnly(readOnly); retval = obj.body(); if (doRndRollback) { logger.warn("randomly rollback the transaction"); throw new LockAcquisitionException("Random Rollback", new SQLException("Random Rollback")); } if (flushMode != null) { sn.setFlushMode(flushMode); } tx.commit(); break; // must come right after commit } catch (Throwable ex) { if (tx != null) { if (flushMode != null) { sn.setFlushMode(flushMode); } tx.rollback(); flushTransactionCallbacks(false); } // Strip off the BddException wrapper if a callee added it. Throwable realEx = (ex instanceof BddException) ? ex.getCause() : ex; if (isRetryable(realEx)) { if (retriesLeft > 0) { if (!doRndRollback) { retriesLeft--; reportRetry(retriesLeft, realEx); } } else { throw TxRetryException.wrap(realEx, doRndRollback); } } else if (isUniqViolation(realEx)) { throw UniqueConstraintViolationException.wrap((ConstraintViolationException) realEx); } else { throw BddException.wrapIfNeeded(ex, "Exception in a DAL transaction"); } } } flushTransactionCallbacks(true); return retval; } /** * Enters a checked region: either a read-write transaction or a blocking * operation (only VC operations, at the moment). That is, we do not allow * read-write transactions in VC sessions, and we do not allow VC sessions * in read-write transactions. The goal is to avoid conflicts between * two long-running read-write transactions. **/ private static void enterCheckedRegion() { Boolean oldVal = inCheckedRegion.get(); AuAssert.check(oldVal == null || !oldVal); inCheckedRegion.set(true); } /** * Leaves a checked region. **/ private static void leaveCheckedRegion() { AuAssert.check(inCheckedRegion.get()); inCheckedRegion.set(false); } /** * Helper routine for wrapping a piece of code in a Hibernate transaction. * * @param obj -- the body of the transaction. It may be executed multiple * times if a retryable exception is encountered. **/ public static <T> T inTransactionDo(Saveable<T> obj) { return inTransactionDoWork(obj, false, defaultRetries); } /** * Similar to inTransactionDo, but allows only read-only database operations * in this transaction. Attempt to write something from such a transaction * will result in a jdbc exception. We do allow read-only transactions in * VC sessions and vice-versa. **/ public static <T> T inRoTransactionDo(Saveable<T> obj) { return inTransactionDoWork(obj, true, defaultRetries); } /** * Similar to inTransactionDo, but runs the transaction in a checked region. * TODO: enter/leave the checked region directly from inTransactionDo and * get rid of this routine. At the moment however, we have many cases of VC * operations in RW transactions, so it would break. Until that is fixed, * we use explicit inRwTransactionDo for known-good cases, like all annotated * UI and WSDL requests. **/ public static <T> T inRwTransactionDo(Saveable<T> obj) { enterCheckedRegion(); try { return inTransactionDoWork(obj, false, defaultRetries); } finally { leaveCheckedRegion(); } } /** * Helper routine for wrapping a piece of code in a Hibernate transaction if no transaction is active. * It doesn't start a new transaction if it's already in one. * * @param obj -- the body of the transaction. **/ public static <T> T autoTransactionDo(Saveable<T> obj) { if (isInTransaction()) { try { return obj.body(); } catch (Exception exc) { throw BddException.wrapIfNeeded(exc, "Exception in a DAL transaction"); } } else { return inTransactionDo(obj); } } /** * Fetch and return an object of the given class and id from the database. * * @param aClass -- the class of the object. * @param id -- the id of the object. * @return The reference to the object or null if the object is not found. * @throws HibernateException -- on Hibernate errors. **/ public static <T> T findById(final Class<T> aClass, final Serializable id) { return autoTransactionDo(new Saveable<T>() { @SuppressWarnings("unchecked") public T body() { return (T) getSession().get(aClass, id); } }); } /** * Fetch and return a list of objects of the given class that * satisfy the given criteria. * * @param aClass -- the class of the object. * @param queryCriteria -- the criteria to filter the results. * @return The list of matching objects. * @throws HibernateException -- on Hibernate errors. **/ public static <T> List<T> findByCriteria(final Class<T> aClass, final Criterion... queryCriteria) { return findByCriteria(aClass, null, null, null, queryCriteria); } /** * Fetch and return an object that satisfies the given criteria. * * @param aClass -- the class of the object. * @param queryCriteria -- the criteria to filter the results. * @return The matching object or null if the object is not found. * If the object is not unique, an assertion is thrown. **/ public static <T> T findUniqueByCriteria(Class<T> aClass, Criterion... queryCriteria) { List<T> results = findByCriteria(aClass, queryCriteria); if (results.size() == 0) { return null; } else { AuAssert.check(results.size() == 1); return results.get(0); } } /** * Fetch and return a list of objects of the given class which * is a subset of the all the records that satisfy the given * criteria. The subset is specified by parameter firstResult * and maxResults. * * @param aClass -- the class of the object. * @param order -- the result set order. * @param firstResult -- the offset in the query result where the returned list starts. * @param maxResults -- the maximum records returned. * @param queryCriteria -- the criteria to filter the results. * @return The list of matching objects. * @throws HibernateException -- on Hibernate errors. */ private static <T> List<T> findByCriteria(final Class<T> aClass, final Order[] order, final Integer firstResult, final Integer maxResults, final Criterion... queryCriteria) { return autoTransactionDo(new Saveable<List<T>>() { @SuppressWarnings("unchecked") public List<T> body() { Criteria criteria = getSession().createCriteria(aClass); for (Criterion c : queryCriteria) { criteria.add(c); } if (order != null) { for (Order o : order) { criteria.addOrder(o); } } if (firstResult != null) { criteria.setFirstResult(firstResult); } if (maxResults != null) { criteria.setMaxResults(maxResults); criteria.setFetchSize(maxResults); // just a hint } return criteria.list(); } }); } /** * Fetch and return a list of all objects of the given class from * the database. * * @param aClass -- the class of the object. * @throws HibernateException -- on Hibernate errors. **/ public static <T> List<T> findAll(Class<T> aClass) { return findByCriteria(aClass); } /** * Fetch and return a list of objects of the given class from * the database in the requested <code>order</code>. * * @param aClass -- the class of the object. * @param order -- the result set order. * @throws HibernateException -- on Hibernate errors. **/ public static <T> List<T> findAll(Class<T> aClass, Order[] order) { return findByCriteria(aClass, order, null, null); } /** * Save a newly-created object to the database. * * @param obj -- the object to save. * @throws HibernateException -- on Hibernate errors. **/ public static void insert(Object obj) { getSession().save(obj); } /** * Update a persistent object in the database. * * @param obj -- the object to save. * @throws HibernateException -- on Hibernate errors. **/ public static void update(Object obj) { getSession().update(obj); } /** * Delete the given object from the database. * * @param obj -- the object to delete. * @throws HibernateException -- on Hibernate errors. **/ public static void delete(Object obj) { getSession().delete(obj); } /** * Refresh/reload the given object from the database. * * @param obj -- the object to refresh. * @throws HibernateException -- on Hibernate errors. **/ public static void refresh(Object obj) { getSession().refresh(obj); } public static void inTransactionUpdate(final Object obj) { inTransactionDo(new Saveable<Void>() { public Void body() { update(obj); return null; } }); } public static void inTransactionRefresh(final Object obj) { inTransactionDo(new Saveable<Void>() { public Void body() { refresh(obj); return null; } }); } public static void inTransactionDelete(final Object obj) { inTransactionDo(new Saveable<Void>() { public Void body() { delete(obj); return null; } }); } /* * API for transaction-complete callbacks */ private interface Callback { void onTransactionComplete(boolean committed); } private static ThreadLocal<ArrayList<Callback>> txCompleteCallbacks = new ThreadLocal<ArrayList<Callback>>(); // no synchronization needed, as txCompleteCallbacks is thread local // we don't have to care which transaction, until/unless nested transactions are allowed private static void flushTransactionCallbacks(boolean committed) { AuAssert.check(!isInTransaction()); ArrayList<Callback> cbs = txCompleteCallbacks.get(); if (cbs != null) { txCompleteCallbacks.set(null); for (Callback cb : cbs) { cb.onTransactionComplete(committed); } } } }