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 */ 020package org.sonar.jpa.session; 021 022import com.google.common.annotations.VisibleForTesting; 023import com.google.common.collect.Maps; 024import org.apache.commons.lang.StringUtils; 025import org.sonar.api.database.DatabaseSession; 026 027import javax.persistence.EntityManager; 028import javax.persistence.NonUniqueResultException; 029import javax.persistence.PersistenceException; 030import javax.persistence.Query; 031import java.util.*; 032 033public 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 @Override 049 public EntityManager getEntityManager() { 050 return entityManager; 051 } 052 053 @Override 054 public void start() { 055 entityManager = connector.createEntityManager(); 056 index = 0; 057 } 058 059 @Override 060 public void stop() { 061 commit(); 062 if (entityManager != null && entityManager.isOpen()) { 063 entityManager.close(); 064 entityManager = null; 065 } 066 } 067 068 @Override 069 public void commit() { 070 if (entityManager != null && inTransaction) { 071 if (entityManager.isOpen()) { 072 if (entityManager.getTransaction().getRollbackOnly()) { 073 entityManager.getTransaction().rollback(); 074 } else { 075 entityManager.getTransaction().commit(); 076 } 077 entityManager.clear(); 078 index = 0; 079 } 080 inTransaction = false; 081 } 082 } 083 084 @Override 085 public void rollback() { 086 if (entityManager != null && inTransaction) { 087 entityManager.getTransaction().rollback(); 088 inTransaction = false; 089 } 090 } 091 092 @Override 093 public <T> T save(T model) { 094 startTransaction(); 095 internalSave(model, true); 096 return model; 097 } 098 099 @Override 100 public Object saveWithoutFlush(Object model) { 101 startTransaction(); 102 internalSave(model, false); 103 return model; 104 } 105 106 @Override 107 public boolean contains(Object model) { 108 startTransaction(); 109 return entityManager.contains(model); 110 } 111 112 @Override 113 public void save(Object... models) { 114 startTransaction(); 115 for (Object model : models) { 116 save(model); 117 } 118 } 119 120 private void internalSave(Object model, boolean flushIfNeeded) { 121 try { 122 entityManager.persist(model); 123 } catch (PersistenceException e) { 124 /* 125 * See http://jira.codehaus.org/browse/SONAR-2234 126 * In some cases Hibernate can throw exceptions without meaningful information about context, so we improve them here. 127 */ 128 throw new PersistenceException("Unable to persist : " + model, e); 129 } 130 if (flushIfNeeded && (++index % BATCH_SIZE == 0)) { 131 commit(); 132 } 133 } 134 135 @Override 136 public Object merge(Object model) { 137 startTransaction(); 138 return entityManager.merge(model); 139 } 140 141 @Override 142 public void remove(Object model) { 143 startTransaction(); 144 entityManager.remove(model); 145 if (++index % BATCH_SIZE == 0) { 146 commit(); 147 } 148 } 149 150 @Override 151 public void removeWithoutFlush(Object model) { 152 startTransaction(); 153 entityManager.remove(model); 154 } 155 156 @Override 157 public <T> T reattach(Class<T> entityClass, Object primaryKey) { 158 startTransaction(); 159 return entityManager.getReference(entityClass, primaryKey); 160 } 161 162 private void startTransaction() { 163 if (!inTransaction) { 164 entityManager.getTransaction().begin(); 165 inTransaction = true; 166 } 167 } 168 169 /** 170 * Note that not recommended to directly execute {@link Query#getSingleResult()}, because it will bypass exception handling, 171 * which done in {@link #getSingleResult(Query, Object)}. 172 */ 173 @Override 174 public Query createQuery(String hql) { 175 startTransaction(); 176 return entityManager.createQuery(hql); 177 } 178 179 @Override 180 public Query createNativeQuery(String sql) { 181 startTransaction(); 182 return entityManager.createNativeQuery(sql); 183 } 184 185 /** 186 * @return the result or <code>defaultValue</code>, if not found 187 * @throws NonUniqueResultException if more than one result 188 */ 189 @Override 190 public <T> T getSingleResult(Query query, T defaultValue) { 191 /* 192 * See http://jira.codehaus.org/browse/SONAR-2225 193 * By default Hibernate throws NonUniqueResultException without meaningful information about context, 194 * so we improve it here by adding all results in error message. 195 * Note that in some rare situations we can receive too many results, which may lead to OOME, 196 * but actually it will mean that database is corrupted as we don't expect more than one result 197 * and in fact org.hibernate.ejb.QueryImpl#getSingleResult() anyway does loading of several results under the hood. 198 */ 199 List<T> result = query.getResultList(); 200 201 if (result.size() == 1) { 202 return result.get(0); 203 204 } else if (result.isEmpty()) { 205 return defaultValue; 206 207 } else { 208 Set<T> uniqueResult = new HashSet<T>(result); 209 if (uniqueResult.size() > 1) { 210 throw new NonUniqueResultException("Expected single result, but got : " + result.toString()); 211 } else { 212 return uniqueResult.iterator().next(); 213 } 214 } 215 } 216 217 @Override 218 public <T> T getEntity(Class<T> entityClass, Object id) { 219 startTransaction(); 220 return getEntityManager().find(entityClass, id); 221 } 222 223 /** 224 * @return the result or <code>null</code>, if not found 225 * @throws NonUniqueResultException if more than one result 226 */ 227 @Override 228 public <T> T getSingleResult(Class<T> entityClass, Object... criterias) { 229 try { 230 return getSingleResult(getQueryForCriterias(entityClass, true, criterias), (T) null); 231 232 } catch (NonUniqueResultException ex) { 233 NonUniqueResultException e = new NonUniqueResultException("Expected single result for entitiy " + entityClass.getSimpleName() 234 + " with criterias : " + StringUtils.join(criterias, ",")); 235 throw (NonUniqueResultException) e.initCause(ex); 236 } 237 } 238 239 @Override 240 public <T> List<T> getResults(Class<T> entityClass, Object... criterias) { 241 return getQueryForCriterias(entityClass, true, criterias).getResultList(); 242 } 243 244 @Override 245 public <T> List<T> getResults(Class<T> entityClass) { 246 return getQueryForCriterias(entityClass, false, (Object[]) null).getResultList(); 247 } 248 249 private Query getQueryForCriterias(Class<?> entityClass, boolean raiseError, Object... criterias) { 250 if (criterias == null && raiseError) { 251 throw new IllegalStateException("criterias parameter must be provided"); 252 } 253 startTransaction(); 254 StringBuilder hql = new StringBuilder("SELECT o FROM ").append(entityClass.getSimpleName()).append(" o"); 255 if (criterias != null) { 256 hql.append(" WHERE "); 257 Map<String, Object> mappedCriterias = Maps.newHashMap(); 258 for (int i = 0; i < criterias.length; i += 2) { 259 mappedCriterias.put((String) criterias[i], criterias[i + 1]); 260 } 261 buildCriteriasHQL(hql, mappedCriterias); 262 Query query = getEntityManager().createQuery(hql.toString()); 263 264 for (Map.Entry<String, Object> entry : mappedCriterias.entrySet()) { 265 if (entry.getValue() != null) { 266 query.setParameter(entry.getKey(), entry.getValue()); 267 } 268 } 269 return query; 270 } 271 return getEntityManager().createQuery(hql.toString()); 272 } 273 274 @VisibleForTesting 275 void buildCriteriasHQL(StringBuilder hql, Map<String, Object> mappedCriterias) { 276 for (Iterator<Map.Entry<String, Object>> i = mappedCriterias.entrySet().iterator(); i.hasNext();) { 277 Map.Entry<String, Object> entry = i.next(); 278 hql.append("o.").append(entry.getKey()); 279 if (entry.getValue() == null) { 280 hql.append(" IS NULL"); 281 } else { 282 hql.append("=:").append(entry.getKey()); 283 } 284 if (i.hasNext()) { 285 hql.append(" AND "); 286 } 287 } 288 } 289 290}