1 /*
2 * Hibernate, Relational Persistence for Idiomatic Java
3 *
4 * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as
5 * indicated by the @author tags or express copyright attribution
6 * statements applied by the authors. All third-party contributions are
7 * distributed under license by Red Hat Middleware LLC.
8 *
9 * This copyrighted material is made available to anyone wishing to use, modify,
10 * copy, or redistribute it subject to the terms and conditions of the GNU
11 * Lesser General Public License, as published by the Free Software Foundation.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
16 * for more details.
17 *
18 * You should have received a copy of the GNU Lesser General Public License
19 * along with this distribution; if not, write to:
20 * Free Software Foundation, Inc.
21 * 51 Franklin Street, Fifth Floor
22 * Boston, MA 02110-1301 USA
23 *
24 */
25 package org.hibernate.jdbc;
26
27 import java.sql.CallableStatement;
28 import java.sql.Connection;
29 import java.sql.PreparedStatement;
30 import java.sql.ResultSet;
31 import java.sql.SQLException;
32 import java.util.HashSet;
33 import java.util.Iterator;
34 import java.util.ConcurrentModificationException;
35
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 import org.hibernate.AssertionFailure;
40 import org.hibernate.HibernateException;
41 import org.hibernate.Interceptor;
42 import org.hibernate.ScrollMode;
43 import org.hibernate.TransactionException;
44 import org.hibernate.dialect.Dialect;
45 import org.hibernate.engine.SessionFactoryImplementor;
46 import org.hibernate.exception.JDBCExceptionHelper;
47 import org.hibernate.jdbc.util.FormatStyle;
48 import org.hibernate.util.JDBCExceptionReporter;
49
50 /**
51 * Manages prepared statements and batching.
52 *
53 * @author Gavin King
54 */
55 public abstract class AbstractBatcher implements Batcher {
56
57 private static int globalOpenPreparedStatementCount;
58 private static int globalOpenResultSetCount;
59
60 private int openPreparedStatementCount;
61 private int openResultSetCount;
62
63 protected static final Logger log = LoggerFactory.getLogger( AbstractBatcher.class );
64
65 private final ConnectionManager connectionManager;
66 private final SessionFactoryImplementor factory;
67
68 private PreparedStatement batchUpdate;
69 private String batchUpdateSQL;
70
71 private HashSet statementsToClose = new HashSet();
72 private HashSet resultSetsToClose = new HashSet();
73 private PreparedStatement lastQuery;
74
75 private boolean releasing = false;
76 private final Interceptor interceptor;
77
78 private long transactionTimeout = -1;
79 boolean isTransactionTimeoutSet;
80
81 public AbstractBatcher(ConnectionManager connectionManager, Interceptor interceptor) {
82 this.connectionManager = connectionManager;
83 this.interceptor = interceptor;
84 this.factory = connectionManager.getFactory();
85 }
86
87 public void setTransactionTimeout(int seconds) {
88 isTransactionTimeoutSet = true;
89 transactionTimeout = System.currentTimeMillis() / 1000 + seconds;
90 }
91
92 public void unsetTransactionTimeout() {
93 isTransactionTimeoutSet = false;
94 }
95
96 protected PreparedStatement getStatement() {
97 return batchUpdate;
98 }
99
100 public CallableStatement prepareCallableStatement(String sql)
101 throws SQLException, HibernateException {
102 executeBatch();
103 logOpenPreparedStatement();
104 return getCallableStatement( connectionManager.getConnection(), sql, false);
105 }
106
107 public PreparedStatement prepareStatement(String sql)
108 throws SQLException, HibernateException {
109 return prepareStatement( sql, false );
110 }
111
112 public PreparedStatement prepareStatement(String sql, boolean getGeneratedKeys)
113 throws SQLException, HibernateException {
114 executeBatch();
115 logOpenPreparedStatement();
116 return getPreparedStatement(
117 connectionManager.getConnection(),
118 sql,
119 false,
120 getGeneratedKeys,
121 null,
122 null,
123 false
124 );
125 }
126
127 public PreparedStatement prepareStatement(String sql, String[] columnNames)
128 throws SQLException, HibernateException {
129 executeBatch();
130 logOpenPreparedStatement();
131 return getPreparedStatement(
132 connectionManager.getConnection(),
133 sql,
134 false,
135 false,
136 columnNames,
137 null,
138 false
139 );
140 }
141
142 public PreparedStatement prepareSelectStatement(String sql)
143 throws SQLException, HibernateException {
144 logOpenPreparedStatement();
145 return getPreparedStatement(
146 connectionManager.getConnection(),
147 sql,
148 false,
149 false,
150 null,
151 null,
152 false
153 );
154 }
155
156 public PreparedStatement prepareQueryStatement(
157 String sql,
158 boolean scrollable,
159 ScrollMode scrollMode) throws SQLException, HibernateException {
160 logOpenPreparedStatement();
161 PreparedStatement ps = getPreparedStatement(
162 connectionManager.getConnection(),
163 sql,
164 scrollable,
165 scrollMode
166 );
167 setStatementFetchSize( ps );
168 statementsToClose.add( ps );
169 lastQuery = ps;
170 return ps;
171 }
172
173 public CallableStatement prepareCallableQueryStatement(
174 String sql,
175 boolean scrollable,
176 ScrollMode scrollMode) throws SQLException, HibernateException {
177 logOpenPreparedStatement();
178 CallableStatement ps = ( CallableStatement ) getPreparedStatement(
179 connectionManager.getConnection(),
180 sql,
181 scrollable,
182 false,
183 null,
184 scrollMode,
185 true
186 );
187 setStatementFetchSize( ps );
188 statementsToClose.add( ps );
189 lastQuery = ps;
190 return ps;
191 }
192
193 public void abortBatch(SQLException sqle) {
194 try {
195 if (batchUpdate!=null) closeStatement(batchUpdate);
196 }
197 catch (SQLException e) {
198 //noncritical, swallow and let the other propagate!
199 JDBCExceptionReporter.logExceptions(e);
200 }
201 finally {
202 batchUpdate=null;
203 batchUpdateSQL=null;
204 }
205 }
206
207 public ResultSet getResultSet(PreparedStatement ps) throws SQLException {
208 ResultSet rs = ps.executeQuery();
209 resultSetsToClose.add(rs);
210 logOpenResults();
211 return rs;
212 }
213
214 public ResultSet getResultSet(CallableStatement ps, Dialect dialect) throws SQLException {
215 ResultSet rs = dialect.getResultSet(ps);
216 resultSetsToClose.add(rs);
217 logOpenResults();
218 return rs;
219
220 }
221
222 public void closeQueryStatement(PreparedStatement ps, ResultSet rs) throws SQLException {
223 boolean psStillThere = statementsToClose.remove( ps );
224 try {
225 if ( rs != null ) {
226 if ( resultSetsToClose.remove( rs ) ) {
227 logCloseResults();
228 rs.close();
229 }
230 }
231 }
232 finally {
233 if ( psStillThere ) {
234 closeQueryStatement( ps );
235 }
236 }
237 }
238
239 public PreparedStatement prepareBatchStatement(String sql)
240 throws SQLException, HibernateException {
241 sql = getSQL( sql );
242
243 if ( !sql.equals(batchUpdateSQL) ) {
244 batchUpdate=prepareStatement(sql); // calls executeBatch()
245 batchUpdateSQL=sql;
246 }
247 else {
248 log.debug("reusing prepared statement");
249 log(sql);
250 }
251 return batchUpdate;
252 }
253
254 public CallableStatement prepareBatchCallableStatement(String sql)
255 throws SQLException, HibernateException {
256 if ( !sql.equals(batchUpdateSQL) ) { // TODO: what if batchUpdate is a callablestatement ?
257 batchUpdate=prepareCallableStatement(sql); // calls executeBatch()
258 batchUpdateSQL=sql;
259 }
260 return (CallableStatement)batchUpdate;
261 }
262
263
264 public void executeBatch() throws HibernateException {
265 if (batchUpdate!=null) {
266 try {
267 try {
268 doExecuteBatch(batchUpdate);
269 }
270 finally {
271 closeStatement(batchUpdate);
272 }
273 }
274 catch (SQLException sqle) {
275 throw JDBCExceptionHelper.convert(
276 factory.getSQLExceptionConverter(),
277 sqle,
278 "Could not execute JDBC batch update",
279 batchUpdateSQL
280 );
281 }
282 finally {
283 batchUpdate=null;
284 batchUpdateSQL=null;
285 }
286 }
287 }
288
289 public void closeStatement(PreparedStatement ps) throws SQLException {
290 logClosePreparedStatement();
291 closePreparedStatement(ps);
292 }
293
294 private void closeQueryStatement(PreparedStatement ps) throws SQLException {
295
296 try {
297 //work around a bug in all known connection pools....
298 if ( ps.getMaxRows()!=0 ) ps.setMaxRows(0);
299 if ( ps.getQueryTimeout()!=0 ) ps.setQueryTimeout(0);
300 }
301 catch (Exception e) {
302 log.warn("exception clearing maxRows/queryTimeout", e);
303 // ps.close(); //just close it; do NOT try to return it to the pool!
304 return; //NOTE: early exit!
305 }
306 finally {
307 closeStatement(ps);
308 }
309
310 if ( lastQuery==ps ) lastQuery = null;
311
312 }
313
314 /**
315 * Actually releases the batcher, allowing it to cleanup internally held
316 * resources.
317 */
318 public void closeStatements() {
319 try {
320 releasing = true;
321
322 try {
323 if ( batchUpdate != null ) {
324 batchUpdate.close();
325 }
326 }
327 catch ( SQLException sqle ) {
328 //no big deal
329 log.warn( "Could not close a JDBC prepared statement", sqle );
330 }
331 batchUpdate = null;
332 batchUpdateSQL = null;
333
334 Iterator iter = resultSetsToClose.iterator();
335 while ( iter.hasNext() ) {
336 try {
337 logCloseResults();
338 ( ( ResultSet ) iter.next() ).close();
339 }
340 catch ( SQLException e ) {
341 // no big deal
342 log.warn( "Could not close a JDBC result set", e );
343 }
344 catch ( ConcurrentModificationException e ) {
345 // this has been shown to happen occasionally in rare cases
346 // when using a transaction manager + transaction-timeout
347 // where the timeout calls back through Hibernate's
348 // registered transaction synchronization on a separate
349 // "reaping" thread. In cases where that reaping thread
350 // executes through this block at the same time the main
351 // application thread does we can get into situations where
352 // these CMEs occur. And though it is not "allowed" per-se,
353 // the end result without handling it specifically is infinite
354 // looping. So here, we simply break the loop
355 log.info( "encountered CME attempting to release batcher; assuming cause is tx-timeout scenario and ignoring" );
356 break;
357 }
358 catch ( Throwable e ) {
359 // sybase driver (jConnect) throwing NPE here in certain
360 // cases, but we'll just handle the general "unexpected" case
361 log.warn( "Could not close a JDBC result set", e );
362 }
363 }
364 resultSetsToClose.clear();
365
366 iter = statementsToClose.iterator();
367 while ( iter.hasNext() ) {
368 try {
369 closeQueryStatement( ( PreparedStatement ) iter.next() );
370 }
371 catch ( ConcurrentModificationException e ) {
372 // see explanation above...
373 log.info( "encountered CME attempting to release batcher; assuming cause is tx-timeout scenario and ignoring" );
374 break;
375 }
376 catch ( SQLException e ) {
377 // no big deal
378 log.warn( "Could not close a JDBC statement", e );
379 }
380 }
381 statementsToClose.clear();
382 }
383 finally {
384 releasing = false;
385 }
386 }
387
388 protected abstract void doExecuteBatch(PreparedStatement ps) throws SQLException, HibernateException;
389
390 private String preparedStatementCountsToString() {
391 return
392 " (open PreparedStatements: " +
393 openPreparedStatementCount +
394 ", globally: " +
395 globalOpenPreparedStatementCount +
396 ")";
397 }
398
399 private String resultSetCountsToString() {
400 return
401 " (open ResultSets: " +
402 openResultSetCount +
403 ", globally: " +
404 globalOpenResultSetCount +
405 ")";
406 }
407
408 private void logOpenPreparedStatement() {
409 if ( log.isDebugEnabled() ) {
410 log.debug( "about to open PreparedStatement" + preparedStatementCountsToString() );
411 openPreparedStatementCount++;
412 globalOpenPreparedStatementCount++;
413 }
414 }
415
416 private void logClosePreparedStatement() {
417 if ( log.isDebugEnabled() ) {
418 log.debug( "about to close PreparedStatement" + preparedStatementCountsToString() );
419 openPreparedStatementCount--;
420 globalOpenPreparedStatementCount--;
421 }
422 }
423
424 private void logOpenResults() {
425 if ( log.isDebugEnabled() ) {
426 log.debug( "about to open ResultSet" + resultSetCountsToString() );
427 openResultSetCount++;
428 globalOpenResultSetCount++;
429 }
430 }
431 private void logCloseResults() {
432 if ( log.isDebugEnabled() ) {
433 log.debug( "about to close ResultSet" + resultSetCountsToString() );
434 openResultSetCount--;
435 globalOpenResultSetCount--;
436 }
437 }
438
439 protected SessionFactoryImplementor getFactory() {
440 return factory;
441 }
442
443 private void log(String sql) {
444 factory.getSettings().getSqlStatementLogger().logStatement( sql, FormatStyle.BASIC );
445 }
446
447 private PreparedStatement getPreparedStatement(
448 final Connection conn,
449 final String sql,
450 final boolean scrollable,
451 final ScrollMode scrollMode) throws SQLException {
452 return getPreparedStatement(
453 conn,
454 sql,
455 scrollable,
456 false,
457 null,
458 scrollMode,
459 false
460 );
461 }
462
463 private CallableStatement getCallableStatement(
464 final Connection conn,
465 String sql,
466 boolean scrollable) throws SQLException {
467 if ( scrollable && !factory.getSettings().isScrollableResultSetsEnabled() ) {
468 throw new AssertionFailure("scrollable result sets are not enabled");
469 }
470
471 sql = getSQL( sql );
472 log( sql );
473
474 log.trace("preparing callable statement");
475 if ( scrollable ) {
476 return conn.prepareCall(
477 sql,
478 ResultSet.TYPE_SCROLL_INSENSITIVE,
479 ResultSet.CONCUR_READ_ONLY
480 );
481 }
482 else {
483 return conn.prepareCall( sql );
484 }
485 }
486
487 private String getSQL(String sql) {
488 sql = interceptor.onPrepareStatement( sql );
489 if ( sql==null || sql.length() == 0 ) {
490 throw new AssertionFailure( "Interceptor.onPrepareStatement() returned null or empty string." );
491 }
492 return sql;
493 }
494
495 private PreparedStatement getPreparedStatement(
496 final Connection conn,
497 String sql,
498 boolean scrollable,
499 final boolean useGetGeneratedKeys,
500 final String[] namedGeneratedKeys,
501 final ScrollMode scrollMode,
502 final boolean callable) throws SQLException {
503 if ( scrollable && !factory.getSettings().isScrollableResultSetsEnabled() ) {
504 throw new AssertionFailure("scrollable result sets are not enabled");
505 }
506 if ( useGetGeneratedKeys && !factory.getSettings().isGetGeneratedKeysEnabled() ) {
507 throw new AssertionFailure("getGeneratedKeys() support is not enabled");
508 }
509
510 sql = getSQL( sql );
511 log( sql );
512
513 log.trace( "preparing statement" );
514 PreparedStatement result;
515 if ( scrollable ) {
516 if ( callable ) {
517 result = conn.prepareCall( sql, scrollMode.toResultSetType(), ResultSet.CONCUR_READ_ONLY );
518 }
519 else {
520 result = conn.prepareStatement( sql, scrollMode.toResultSetType(), ResultSet.CONCUR_READ_ONLY );
521 }
522 }
523 else if ( useGetGeneratedKeys ) {
524 result = conn.prepareStatement( sql, PreparedStatement.RETURN_GENERATED_KEYS );
525 }
526 else if ( namedGeneratedKeys != null ) {
527 result = conn.prepareStatement( sql, namedGeneratedKeys );
528 }
529 else {
530 if ( callable ) {
531 result = conn.prepareCall( sql );
532 }
533 else {
534 result = conn.prepareStatement( sql );
535 }
536 }
537
538 setTimeout( result );
539
540 if ( factory.getStatistics().isStatisticsEnabled() ) {
541 factory.getStatisticsImplementor().prepareStatement();
542 }
543
544 return result;
545
546 }
547
548 private void setTimeout(PreparedStatement result) throws SQLException {
549 if ( isTransactionTimeoutSet ) {
550 int timeout = (int) ( transactionTimeout - ( System.currentTimeMillis() / 1000 ) );
551 if (timeout<=0) {
552 throw new TransactionException("transaction timeout expired");
553 }
554 else {
555 result.setQueryTimeout(timeout);
556 }
557 }
558 }
559
560 private void closePreparedStatement(PreparedStatement ps) throws SQLException {
561 try {
562 log.trace("closing statement");
563 ps.close();
564 if ( factory.getStatistics().isStatisticsEnabled() ) {
565 factory.getStatisticsImplementor().closeStatement();
566 }
567 }
568 finally {
569 if ( !releasing ) {
570 // If we are in the process of releasing, no sense
571 // checking for aggressive-release possibility.
572 connectionManager.afterStatement();
573 }
574 }
575 }
576
577 private void setStatementFetchSize(PreparedStatement statement) throws SQLException {
578 Integer statementFetchSize = factory.getSettings().getJdbcFetchSize();
579 if ( statementFetchSize!=null ) {
580 statement.setFetchSize( statementFetchSize.intValue() );
581 }
582 }
583
584 public Connection openConnection() throws HibernateException {
585 log.debug("opening JDBC connection");
586 try {
587 return factory.getConnectionProvider().getConnection();
588 }
589 catch (SQLException sqle) {
590 throw JDBCExceptionHelper.convert(
591 factory.getSQLExceptionConverter(),
592 sqle,
593 "Cannot open connection"
594 );
595 }
596 }
597
598 public void closeConnection(Connection conn) throws HibernateException {
599 if ( conn == null ) {
600 log.debug( "found null connection on AbstractBatcher#closeConnection" );
601 // EARLY EXIT!!!!
602 return;
603 }
604
605 if ( log.isDebugEnabled() ) {
606 log.debug( "closing JDBC connection" + preparedStatementCountsToString() + resultSetCountsToString() );
607 }
608
609 try {
610 if ( !conn.isClosed() ) {
611 JDBCExceptionReporter.logAndClearWarnings( conn );
612 }
613 factory.getConnectionProvider().closeConnection( conn );
614 }
615 catch ( SQLException sqle ) {
616 throw JDBCExceptionHelper.convert( factory.getSQLExceptionConverter(), sqle, "Cannot close connection" );
617 }
618 }
619
620 public void cancelLastQuery() throws HibernateException {
621 try {
622 if (lastQuery!=null) lastQuery.cancel();
623 }
624 catch (SQLException sqle) {
625 throw JDBCExceptionHelper.convert(
626 factory.getSQLExceptionConverter(),
627 sqle,
628 "Cannot cancel query"
629 );
630 }
631 }
632
633 public boolean hasOpenResources() {
634 return resultSetsToClose.size() > 0 || statementsToClose.size() > 0;
635 }
636
637 public String openResourceStatsAsString() {
638 return preparedStatementCountsToString() + resultSetCountsToString();
639 }
640
641 }
642
643
644
645
646
647