/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. * <p> */ package org.olat.core.commons.persistence; import java.sql.Driver; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Enumeration; import java.util.List; import java.util.Properties; import javax.persistence.Cache; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.Persistence; import javax.persistence.RollbackException; import org.hibernate.HibernateException; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.jpa.HibernateEntityManager; import org.hibernate.jpa.HibernateEntityManagerFactory; import org.hibernate.stat.Statistics; import org.hibernate.type.Type; import org.infinispan.manager.EmbeddedCacheManager; import org.olat.core.configuration.Destroyable; import org.olat.core.id.Persistable; import org.olat.core.logging.AssertException; import org.olat.core.logging.DBRuntimeException; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; /** * A <b>DB </b> is a central place to get a Entity Managers. It acts as a * facade to the database, transactions and Queries. The hibernateSession is * lazy loaded per thread. * * @author Andreas Ch. Kapp * @author Christian Guretzki */ public class DBImpl implements DB, Destroyable { private static final OLog log = Tracing.createLoggerFor(DBImpl.class); private static final int MAX_DB_ACCESS_COUNT = 500; private static DBImpl INSTANCE; private String dbVendor; private static EntityManagerFactory emf; private final ThreadLocal<ThreadLocalData> data = new ThreadLocal<ThreadLocalData>(); // Max value for commit-counter, values over this limit will be logged. private static int maxCommitCounter = 10; /** * [used by spring] */ public DBImpl(Properties databaseProperties) { if(INSTANCE == null) { INSTANCE = this; try { emf = Persistence.createEntityManagerFactory("default", databaseProperties); } catch (Exception e) { e.printStackTrace(); log.error("", e); throw e; } } } protected static DBImpl getInstance() { return INSTANCE; } @Override public boolean isMySQL() { return "mysql".equals(dbVendor); } @Override public boolean isPostgreSQL() { return "postgresql".equals(dbVendor); } @Override public boolean isOracle() { return "oracle".equals(dbVendor); } @Override public String getDbVendor() { return dbVendor; } /** * [used by spring] * @param dbVendor */ public void setDbVendor(String dbVendor) { this.dbVendor = dbVendor; } /** * A <b>ThreadLocalData</b> is used as a central place to store data on a per * thread basis. * * @author Andreas CH. Kapp * @author Christian Guretzki */ protected class ThreadLocalData { private boolean error; private Exception lastError; private boolean initialized = false; // count number of db access in beginTransaction, used to log warn 'to many db access in one transaction' private int accessCounter = 0; // count number of commit in db-session, used to log warn 'Call more than one commit in a db-session' private int commitCounter = 0; private EntityManager em; private ThreadLocalData() { // don't let any other class instantiate ThreadLocalData. } public EntityManager getEntityManager(boolean createIfNecessary) { if(em == null && createIfNecessary) { em = emf.createEntityManager(); } return em; } public EntityManager renewEntityManager() { if(em != null && !em.isOpen()) { try { em.close(); } catch (Exception e) { log.error("", e); } em = null; } return getEntityManager(true); } public void removeEntityManager() { em = null; } public boolean hasTransaction() { if(em != null && em.isOpen()) { EntityTransaction trx = em.getTransaction(); return trx != null && trx.isActive(); } return false; } /** * @return true if initialized. */ protected boolean isInitialized() { return initialized; } protected void setInitialized(boolean b) { initialized = b; } public boolean isError() { if(em != null && em.isOpen()) { EntityTransaction trx = em.getTransaction(); if (trx != null && trx.isActive()) { return trx.getRollbackOnly(); } } return error; } public void setError(boolean error) { this.error = error; } public Exception getLastError() { return lastError; } public void setError(Exception ex) { this.lastError = ex; this.error = true; } protected void incrementAccessCounter() { this.accessCounter++; } protected int getAccessCounter() { return this.accessCounter; } protected void resetAccessCounter() { this.accessCounter = 0; } protected void incrementCommitCounter() { this.commitCounter++; } protected int getCommitCounter() { return this.commitCounter; } protected void resetCommitCounter() { this.commitCounter = 0; } } private void setData(ThreadLocalData data) { this.data.set(data); } private ThreadLocalData getData() { ThreadLocalData tld = data.get(); if (tld == null) { tld = new ThreadLocalData(); setData(tld); } return tld; } @Override public EntityManager getCurrentEntityManager() { //if spring has already an entity manager in this thread bounded, return it EntityManager threadBoundedEm = getData().getEntityManager(true); if(threadBoundedEm != null && threadBoundedEm.isOpen()) { EntityTransaction trx = threadBoundedEm.getTransaction(); //if not active begin a new one (possibly manual committed) if(!trx.isActive()) { trx.begin(); } updateDataStatistics("entityManager"); return threadBoundedEm; } else if(threadBoundedEm == null || !threadBoundedEm.isOpen()) { threadBoundedEm = getData().renewEntityManager(); } EntityTransaction trx = threadBoundedEm.getTransaction(); //if not active begin a new one (possibly manual committed) if(!trx.isActive()) { trx.begin(); } updateDataStatistics("entityManager"); return threadBoundedEm; } private Session getSession(EntityManager em) { return em.unwrap(HibernateEntityManager.class).getSession(); } private boolean unusableTrx(EntityTransaction trx) { return trx == null || !trx.isActive() || trx.getRollbackOnly(); } private void updateDataStatistics(Object logObject) { if (getData().getAccessCounter() > MAX_DB_ACCESS_COUNT) { log.warn("beginTransaction bulk-change, too many db access for one transaction, could be a performance problem (add closeSession/createSession in loop) logObject=" + logObject, null); getData().resetAccessCounter(); } else { getData().incrementAccessCounter(); } } /** * Close the database session. */ @Override public void closeSession() { getData().resetAccessCounter(); // Note: closeSession() now also checks if the connection is open at all // in OLAT-4318 a situation is described where commit() fails and closeSession() // is not called at all. that was due to a call to commit() with a session // that was closed underneath by hibernate (not noticed by DBImpl). // in order to be robust for any similar situation, we check if the // connection is open, otherwise we shouldn't worry about doing any commit/rollback anyway //commit //getCurrentEntityManager(); EntityManager s = getData().getEntityManager(false); if(s != null) { EntityTransaction trx = s.getTransaction(); if(trx.isActive()) { try { trx.commit(); } catch (RollbackException ex) { //possible if trx setRollbackonly log.warn("Close session with transaction set with setRollbackOnly", ex); } catch (Exception e) { log.error("", e); trx.rollback(); } } s.close(); } data.remove(); } private boolean contains(Object object) { EntityManager em = getCurrentEntityManager(); return em.contains(object); } /** * Create a DBQuery * * @param query * @return DBQuery */ @Override public DBQuery createQuery(String query) { try { EntityManager em = getCurrentEntityManager(); Query q = getSession(em).createQuery(query); return new DBQueryImpl(q); } catch (HibernateException he) { getData().setError(he); throw new DBRuntimeException("Error while creating DBQueryImpl: ", he); } } /** * Delete an object. * * @param object */ @Override public void deleteObject(Object object) { EntityManager em = getCurrentEntityManager(); EntityTransaction trx = em.getTransaction(); if (unusableTrx(trx)) { // some program bug throw new DBRuntimeException("cannot delete in a transaction that is rolledback or committed " + object); } try { Object relaoded = em.merge(object); em.remove(relaoded); if (log.isDebug()) { log.debug("delete (trans "+trx.hashCode()+") class "+object.getClass().getName()+" = "+object.toString()); } } catch (HibernateException e) { // we have some error trx.setRollbackOnly(); getData().setError(e); throw new DBRuntimeException("Delete of object failed: " + object, e); } } /** * Deletion query. * * @param query * @param value * @param type * @return nr of deleted rows */ @Override public int delete(String query, Object value, Type type) { int deleted = 0; EntityManager em = getCurrentEntityManager(); EntityTransaction trx = em.getTransaction(); if (unusableTrx(trx)) { // some program bug throw new DBRuntimeException("cannot delete in a transaction that is rolledback or committed " + value); } try { //old: deleted = getSession().delete(query, value, type); Session si = getSession(em); Query qu = si.createQuery(query); qu.setParameter(0, value, type); List foundToDel = qu.list(); int deletionCount = foundToDel.size(); for (int i = 0; i < deletionCount; i++ ) { si.delete( foundToDel.get(i) ); } } catch (HibernateException e) { // we have some error trx.setRollbackOnly(); throw new DBRuntimeException ("Could not delete object: " + value, e); } return deleted; } /** * Deletion query. * * @param query * @param values * @param types * @return nr of deleted rows */ @Override public int delete(String query, Object[] values, Type[] types) { EntityManager em = getCurrentEntityManager(); EntityTransaction trx = em.getTransaction(); if (unusableTrx(trx)) { // some program bug throw new DBRuntimeException("cannot delete in a transaction that is rolledback or committed " + values); } try { //old: deleted = getSession().delete(query, values, types); Session si = getSession(em); Query qu = si.createQuery(query); qu.setParameters(values, types); List foundToDel = qu.list(); int deleted = foundToDel.size(); for (int i = 0; i < deleted; i++ ) { si.delete( foundToDel.get(i) ); } return deleted; } catch (HibernateException e) { // we have some error trx.setRollbackOnly(); throw new DBRuntimeException ("Could not delete object: " + values, e); } } /** * Find objects based on query * * @param query * @param value * @param type * @return List of results. */ @Override public List find(String query, Object value, Type type) { EntityManager em = getCurrentEntityManager(); EntityTransaction trx = em.getTransaction(); try { Query qu = getSession(em).createQuery(query); qu.setParameter(0, value, type); return qu.list(); } catch (HibernateException e) { trx.setRollbackOnly(); String msg = "Find failed in transaction. Query: " + query + " " + e; getData().setError(e); throw new DBRuntimeException(msg, e); } } /** * Find objects based on query * * @param query * @param values * @param types * @return List of results. */ @Override public List find(String query, Object[] values, Type[] types) { EntityManager em = getCurrentEntityManager(); try { // old: li = getSession().find(query, values, types); Query qu = getSession(em).createQuery(query); qu.setParameters(values, types); return qu.list(); } catch (HibernateException e) { em.getTransaction().setRollbackOnly(); getData().setError(e); throw new DBRuntimeException("Find failed in transaction. Query: " + query + " " + e, e); } } /** * Find objects based on query * * @param query * @return List of results. */ @Override public List find(String query) { EntityManager em = getCurrentEntityManager(); try { return em.createQuery(query).getResultList(); } catch (HibernateException e) { em.getTransaction().setRollbackOnly(); getData().setError(e); throw new DBRuntimeException("Find in transaction failed: " + query + " " + e, e); } } /** * Find an object. * * @param theClass * @param key * @return Object, if any found. Null, if non exist. */ @Override public <U> U findObject(Class<U> theClass, Long key) { return getCurrentEntityManager().find(theClass, key); } /** * Load an object. * * @param theClass * @param key * @return Object. */ @Override public <U> U loadObject(Class<U> theClass, Long key) { try { return getCurrentEntityManager().find(theClass, key); } catch (Exception e) { throw new DBRuntimeException("loadObject error: " + theClass + " " + key + " ", e); } } /** * Save an object. * * @param object */ @Override public void saveObject(Object object) { EntityManager em = getCurrentEntityManager(); EntityTransaction trx = em.getTransaction(); if (unusableTrx(trx)) { // some program bug throw new DBRuntimeException("cannot save in a transaction that is rolledback or committed: " + object); } try { em.persist(object); } catch (Exception e) { // we have some error e.printStackTrace(); trx.setRollbackOnly(); getData().setError(e); throw new DBRuntimeException("Save failed in transaction. object: " + object, e); } } /** * Update an object. * * @param object */ @Override public void updateObject(Object object) { EntityManager em = getCurrentEntityManager(); EntityTransaction trx = em.getTransaction(); if (unusableTrx(trx)) { // some program bug throw new DBRuntimeException("cannot update in a transaction that is rolledback or committed " + object); } try { getSession(em).update(object); } catch (HibernateException e) { // we have some error trx.setRollbackOnly(); getData().setError(e); throw new DBRuntimeException("Update object failed in transaction. Query: " + object, e); } } /** * Get any errors from a previous DB call. * * @return Exception, if any. */ public Exception getError() { return getData().getLastError(); } /** * @return True if any errors occured in the previous DB call. */ @Override public boolean isError() { return getData().isError(); } private boolean hasTransaction() { return getData().hasTransaction(); } /** * see DB.loadObject(Persistable persistable, boolean forceReloadFromDB) * * @param persistable * @return the loaded object */ @Override public Persistable loadObject(Persistable persistable) { return loadObject(persistable, false); } /** * loads an object if needed. this makes sense if you have an object which had * been generated in a previous hibernate session AND you need to access a Set * or a attribute which was defined as a proxy. * * @param persistable the object which needs to be reloaded * @param forceReloadFromDB if true, force a reload from the db (e.g. to catch * up to an object commited by another thread which is still in this * thread's session cache * @return the loaded Object */ @Override public Persistable loadObject(Persistable persistable, boolean forceReloadFromDB) { if (persistable == null) throw new AssertException("persistable must not be null"); EntityManager em = getCurrentEntityManager(); Class<? extends Persistable> theClass = persistable.getClass(); if (forceReloadFromDB) { // we want to reload it from the database. // there are 3 scenarios possible: // a) the object is not yet in the hibernate cache // b) the object is in the hibernate cache // c) the object is detached and there is an object with the same id in the hibernate cache if (contains(persistable)) { // case b - then we can use evict and load evict(em, persistable, getData()); return loadObject(theClass, persistable.getKey()); } else { // case a or c - unfortunatelly we can't distinguish these two cases // and session.refresh(Object) doesn't work. // the only scenario that works is load/evict/load Persistable attachedObj = loadObject(theClass, persistable.getKey()); evict(em, attachedObj, getData()); return loadObject(theClass, persistable.getKey()); } } else if (!contains(persistable)) { // forceReloadFromDB is false - hence it is OK to take it from the cache if it would be there // now this object directly is not in the cache, but it's possible that the object is detached // and there is an object with the same id in the hibernate cache. // therefore the following loadObject can either return it from the cache or load it from the DB return loadObject(theClass, persistable.getKey()); } else { // nothing to do, return the same object return persistable; } } private void evict(EntityManager em, Object object, ThreadLocalData localData) { try { getSession(em).evict(object); } catch (Exception e) { localData.setError(e); throw new DBRuntimeException("Error in evict() Object from Database. ", e); } } @Override public void commitAndCloseSession() { try { commit(); } finally { try{ // double check: is the transaction still open? if yes, is it not rolled-back? if yes, do a rollback now! if (hasTransaction() && isError()) { log.error("commitAndCloseSession: commit seems to have failed, transaction still open. Doing a rollback!", new Exception("commitAndCloseSession")); rollback(); } } finally { closeSession(); } } } @Override public void rollbackAndCloseSession() { try { rollback(); } finally { closeSession(); } } /** * Call this to commit a transaction opened by beginTransaction(). */ @Override public void commit() { boolean debug = log.isDebug(); if (debug) log.debug("commit start...", null); try { if (hasTransaction() && !isError()) { if (debug) log.debug("has Transaction and is in Transaction => commit", null); getData().incrementCommitCounter(); if (debug) { if ((maxCommitCounter != 0) && (getData().getCommitCounter() > maxCommitCounter) ) { log.info("Call too many commit in a db-session, commitCounter=" + getData().getCommitCounter() +"; could be a performance problem" , null); } } EntityTransaction trx = getCurrentEntityManager().getTransaction(); if(trx != null) { trx.commit(); } if (debug) log.debug("Commit DONE hasTransaction()=" + hasTransaction(), null); } else if(hasTransaction() && isError()) { EntityTransaction trx = getCurrentEntityManager().getTransaction(); if(trx != null && trx.isActive()) { throw new DBRuntimeException("Try to commit a transaction in error status"); } } else { if (debug) log.debug("Call commit without starting transaction", null ); } } catch (Error er) { log.error("Uncaught Error in DBImpl.commit.", er); throw er; } catch (Exception e) { // Filter Exception form async TaskExecutorThread, there are exception allowed if (!Thread.currentThread().getName().equals("TaskExecutorThread")) { log.warn("Caught Exception in DBImpl.commit.", e); } // Error when trying to commit try { if (hasTransaction()) { EntityTransaction trx = getCurrentEntityManager().getTransaction(); if(trx != null && trx.isActive()) { if(trx.getRollbackOnly()) { try { trx.commit(); } catch (RollbackException e1) { //we wait for this exception } } else { trx.rollback(); } } } } catch (Error er) { log.error("Uncaught Error in DBImpl.commit.catch(Exception).", er); throw er; } catch (Exception ex) { log.warn("Could not rollback transaction after commit!", ex); throw new DBRuntimeException("rollback after commit failed", e); } throw new DBRuntimeException("commit failed, rollback transaction", e); } } /** * Call this to rollback current changes. */ @Override public void rollback() { if (log.isDebug()) log.debug("rollback start...", null); try { // see closeSession() and OLAT-4318: more robustness with commit/rollback/close, therefore // we check if the connection is open at this stage at all EntityTransaction trx = getCurrentEntityManager().getTransaction(); if(trx != null && trx.isActive()) { if(trx.getRollbackOnly()) { try { trx.commit(); } catch (RollbackException e) { //we wait for this exception } } else { trx.rollback(); } } } catch (Exception ex) { log.warn("Could not rollback transaction!",ex); throw new DBRuntimeException("rollback failed", ex); } } /** * Statistics must be enabled first, when you want to use it. * @return Return Hibernates statistics object. */ @Override public Statistics getStatistics() { if(emf instanceof HibernateEntityManagerFactory) { return ((HibernateEntityManagerFactory)emf).getSessionFactory().getStatistics(); } return null; } @Override public EmbeddedCacheManager getCacheContainer() { EmbeddedCacheManager cm; try { Cache cache = emf.getCache(); JpaInfinispanRegionFactory region = cache.unwrap(JpaInfinispanRegionFactory.class); cm = region.getCacheManager(); } catch (Exception e) { log.error("", e); cm = null; } return cm; } /** * @see org.olat.core.commons.persistence.DB#intermediateCommit() */ @Override public void intermediateCommit() { commit(); closeSession(); } @Override public void destroy() { //clean up registered drivers to prevent messages like // The web application [/olat] registered the JBDC driver [com.mysql.Driver] but failed to unregister... Enumeration<Driver> registeredDrivers = DriverManager.getDrivers(); while(registeredDrivers.hasMoreElements()) { try { DriverManager.deregisterDriver(registeredDrivers.nextElement()); } catch (SQLException e) { log.error("Could not unregister database driver.", e); } } } }