001 /* 002 * Sonar, open source software quality management tool. 003 * Copyright (C) 2008-2012 SonarSource 004 * mailto:contact AT sonarsource DOT com 005 * 006 * Sonar is free software; you can redistribute it and/or 007 * modify it under the terms of the GNU Lesser General Public 008 * License as published by the Free Software Foundation; either 009 * version 3 of the License, or (at your option) any later version. 010 * 011 * Sonar is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 * Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public 017 * License along with Sonar; if not, write to the Free Software 018 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 019 */ 020 package org.sonar.jpa.session; 021 022 import com.google.common.annotations.VisibleForTesting; 023 import com.google.common.collect.Maps; 024 import org.apache.commons.lang.StringUtils; 025 import org.sonar.api.database.DatabaseSession; 026 027 import javax.persistence.EntityManager; 028 import javax.persistence.NonUniqueResultException; 029 import javax.persistence.PersistenceException; 030 import javax.persistence.Query; 031 import java.util.*; 032 033 public class JpaDatabaseSession extends DatabaseSession { 034 035 private final DatabaseConnector connector; 036 private EntityManager entityManager = null; 037 private int index = 0; 038 private boolean inTransaction = false; 039 040 public JpaDatabaseSession(DatabaseConnector connector) { 041 this.connector = connector; 042 } 043 044 /** 045 * Note that usage of this method is discouraged, because it allows to construct and execute queries without additional exception handling, 046 * which done in methods of this class. 047 */ 048 public EntityManager getEntityManager() { 049 return entityManager; 050 } 051 052 public void start() { 053 entityManager = connector.createEntityManager(); 054 index = 0; 055 } 056 057 public void stop() { 058 commit(); 059 if (entityManager != null && entityManager.isOpen()) { 060 entityManager.close(); 061 entityManager = null; 062 } 063 } 064 065 public void commit() { 066 if (entityManager != null && inTransaction) { 067 if (entityManager.isOpen()) { 068 if (entityManager.getTransaction().getRollbackOnly()) { 069 entityManager.getTransaction().rollback(); 070 } else { 071 entityManager.getTransaction().commit(); 072 } 073 entityManager.clear(); 074 index = 0; 075 } 076 inTransaction = false; 077 } 078 } 079 080 public void rollback() { 081 if (entityManager != null && inTransaction) { 082 entityManager.getTransaction().rollback(); 083 inTransaction = false; 084 } 085 } 086 087 public <T> T save(T model) { 088 startTransaction(); 089 internalSave(model, true); 090 return model; 091 } 092 093 public Object saveWithoutFlush(Object model) { 094 startTransaction(); 095 internalSave(model, false); 096 return model; 097 } 098 099 public boolean contains(Object model) { 100 startTransaction(); 101 return entityManager.contains(model); 102 } 103 104 public void save(Object... models) { 105 startTransaction(); 106 for (Object model : models) { 107 save(model); 108 } 109 } 110 111 private void internalSave(Object model, boolean flushIfNeeded) { 112 try { 113 entityManager.persist(model); 114 } catch (PersistenceException e) { 115 /* 116 * See http://jira.codehaus.org/browse/SONAR-2234 117 * In some cases Hibernate can throw exceptions without meaningful information about context, so we improve them here. 118 */ 119 throw new PersistenceException("Unable to persist : " + model, e); 120 } 121 if (flushIfNeeded && (++index % BATCH_SIZE == 0)) { 122 commit(); 123 } 124 } 125 126 public Object merge(Object model) { 127 startTransaction(); 128 return entityManager.merge(model); 129 } 130 131 public void remove(Object model) { 132 startTransaction(); 133 entityManager.remove(model); 134 if (++index % BATCH_SIZE == 0) { 135 commit(); 136 } 137 } 138 139 public void removeWithoutFlush(Object model) { 140 startTransaction(); 141 entityManager.remove(model); 142 } 143 144 public <T> T reattach(Class<T> entityClass, Object primaryKey) { 145 startTransaction(); 146 return entityManager.getReference(entityClass, primaryKey); 147 } 148 149 private void startTransaction() { 150 if (!inTransaction) { 151 entityManager.getTransaction().begin(); 152 inTransaction = true; 153 } 154 } 155 156 /** 157 * Note that not recommended to directly execute {@link Query#getSingleResult()}, because it will bypass exception handling, 158 * which done in {@link #getSingleResult(Query, Object)}. 159 */ 160 public Query createQuery(String hql) { 161 startTransaction(); 162 return entityManager.createQuery(hql); 163 } 164 165 @Override 166 public Query createNativeQuery(String sql) { 167 startTransaction(); 168 return entityManager.createNativeQuery(sql); 169 } 170 171 /** 172 * @return the result or <code>defaultValue</code>, if not found 173 * @throws NonUniqueResultException if more than one result 174 */ 175 public <T> T getSingleResult(Query query, T defaultValue) { 176 /* 177 * See http://jira.codehaus.org/browse/SONAR-2225 178 * By default Hibernate throws NonUniqueResultException without meaningful information about context, 179 * so we improve it here by adding all results in error message. 180 * Note that in some rare situations we can receive too many results, which may lead to OOME, 181 * but actually it will mean that database is corrupted as we don't expect more than one result 182 * and in fact org.hibernate.ejb.QueryImpl#getSingleResult() anyway does loading of several results under the hood. 183 */ 184 List<T> result = query.getResultList(); 185 186 if (result.size() == 1) { 187 return result.get(0); 188 189 } else if (result.isEmpty()) { 190 return defaultValue; 191 192 } else { 193 Set<T> uniqueResult = new HashSet<T>(result); 194 if (uniqueResult.size() > 1) { 195 throw new NonUniqueResultException("Expected single result, but got : " + result.toString()); 196 } else { 197 return uniqueResult.iterator().next(); 198 } 199 } 200 } 201 202 public <T> T getEntity(Class<T> entityClass, Object id) { 203 startTransaction(); 204 return getEntityManager().find(entityClass, id); 205 } 206 207 /** 208 * @return the result or <code>null</code>, if not found 209 * @throws NonUniqueResultException if more than one result 210 */ 211 public <T> T getSingleResult(Class<T> entityClass, Object... criterias) { 212 try { 213 return getSingleResult(getQueryForCriterias(entityClass, true, criterias), (T) null); 214 215 } catch (NonUniqueResultException ex) { 216 NonUniqueResultException e = new NonUniqueResultException("Expected single result for entitiy " + entityClass.getSimpleName() 217 + " with criterias : " + StringUtils.join(criterias, ",")); 218 throw (NonUniqueResultException) e.initCause(ex); 219 } 220 } 221 222 public <T> List<T> getResults(Class<T> entityClass, Object... criterias) { 223 return getQueryForCriterias(entityClass, true, criterias).getResultList(); 224 } 225 226 public <T> List<T> getResults(Class<T> entityClass) { 227 return getQueryForCriterias(entityClass, false, (Object[]) null).getResultList(); 228 } 229 230 private Query getQueryForCriterias(Class<?> entityClass, boolean raiseError, Object... criterias) { 231 if (criterias == null && raiseError) { 232 throw new IllegalStateException("criterias parameter must be provided"); 233 } 234 startTransaction(); 235 StringBuilder hql = new StringBuilder("SELECT o FROM ").append(entityClass.getSimpleName()).append(" o"); 236 if (criterias != null) { 237 hql.append(" WHERE "); 238 Map<String, Object> mappedCriterias = Maps.newHashMap(); 239 for (int i = 0; i < criterias.length; i += 2) { 240 mappedCriterias.put((String) criterias[i], criterias[i + 1]); 241 } 242 buildCriteriasHQL(hql, mappedCriterias); 243 Query query = getEntityManager().createQuery(hql.toString()); 244 245 for (Map.Entry<String, Object> entry : mappedCriterias.entrySet()) { 246 if (entry.getValue() != null) { 247 query.setParameter(entry.getKey(), entry.getValue()); 248 } 249 } 250 return query; 251 } 252 return getEntityManager().createQuery(hql.toString()); 253 } 254 255 @VisibleForTesting 256 void buildCriteriasHQL(StringBuilder hql, Map<String, Object> mappedCriterias) { 257 for (Iterator<Map.Entry<String, Object>> i = mappedCriterias.entrySet().iterator(); i.hasNext();) { 258 Map.Entry<String, Object> entry = i.next(); 259 hql.append("o.").append(entry.getKey()); 260 if (entry.getValue() == null) { 261 hql.append(" IS NULL"); 262 } else { 263 hql.append("=:").append(entry.getKey()); 264 } 265 if (i.hasNext()) { 266 hql.append(" AND "); 267 } 268 } 269 } 270 271 }