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 }