Advertisement
Guest User

Untitled

a guest
Feb 7th, 2016
82
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.24 KB | None | 0 0
  1. package com.zz.util.persist;
  2.  
  3. import java.math.BigDecimal;
  4. import java.sql.Connection;
  5. import java.sql.PreparedStatement;
  6. import java.sql.ResultSet;
  7. import java.sql.ResultSetMetaData;
  8. import java.sql.SQLException;
  9. import java.sql.Statement;
  10. import java.sql.Timestamp;
  11. import java.sql.Types;
  12. import java.text.DecimalFormat;
  13. import java.util.ArrayList;
  14. import java.util.Date;
  15. import java.util.Iterator;
  16. import java.util.List;
  17.  
  18. import org.apache.commons.logging.Log;
  19. import org.apache.commons.logging.LogFactory;
  20.  
  21. import com.zz.util.EpocratesException;
  22. import com.zz.util.ValidationException;
  23.  
  24.  
  25.  
  26. /**
  27. * Implements API support for common JDBC operations including convenience methods
  28. * to generate SQL and facilitate usage of prepared statements, connection management,
  29. * and batch handling.
  30. *
  31. * @author zzarate
  32. *
  33. */
  34. public abstract class AbstractJdbcFacade {
  35.  
  36. private static Log log = LogFactory.getLog( AbstractJdbcFacade.class );
  37.  
  38. public final static int DEFAULT_BATCH_THRESHOLD = 10000; // in testing this seemed to offer a good balance of speed/memory performance
  39.  
  40. private String user = null;
  41. private String password = null;
  42.  
  43. private int batchThreshold = DEFAULT_BATCH_THRESHOLD;
  44.  
  45. public AbstractJdbcFacade( String user, String password ) {
  46. this.user = user;
  47. this.password = password;
  48. }
  49.  
  50.  
  51. protected String getUser() {
  52. return user;
  53. }
  54.  
  55.  
  56. protected String getPassword() {
  57. return password;
  58. }
  59.  
  60.  
  61.  
  62. /**
  63. * @param batchThreshold the batchThreshold to set
  64. */
  65. public void setBatchThreshold( int batchThreshold ) {
  66.  
  67. this.batchThreshold = batchThreshold;
  68. }
  69.  
  70.  
  71. /**
  72. * @return the batchThreshold
  73. */
  74. public int getBatchThreshold() {
  75.  
  76. return batchThreshold;
  77. }
  78.  
  79.  
  80. /**
  81. * Obtain connection to data source. Concrete implementations are expected to determine
  82. * how to obtain connection according to specific implementation (e.g. DriverManager, JNDI DataSource, etc.)
  83. *
  84. * @param user
  85. * @param password
  86. * @return
  87. * @throws SQLException
  88. */
  89. public abstract Connection getConnection() throws SQLException;
  90.  
  91.  
  92. /**
  93. * Executes the argument select SQL and returns results as a list of lists.
  94. *
  95. * @param sql
  96. * @return
  97. * @throws SQLException
  98. */
  99. public List<List<?>> select( String sql ) throws SQLException {
  100.  
  101. Connection connection = null;
  102. ResultSet resultSet = null;
  103.  
  104. List<List<?>> list = null;
  105.  
  106.  
  107. if( sql == null || sql.trim().toUpperCase().startsWith( "SELECT" ) == false ) {
  108.  
  109. throw( new SQLException("Invalid SELECT query: " + sql ) );
  110. }
  111.  
  112. if( log.isDebugEnabled() ) {
  113. log.debug( sql );
  114. }
  115.  
  116. Statement statement = null;
  117.  
  118. try {
  119.  
  120. connection = getConnection();
  121. statement = connection.createStatement();
  122.  
  123. resultSet = statement.executeQuery( sql );
  124.  
  125. while( resultSet.next() ) {
  126.  
  127. if( list == null ) {
  128. list = new ArrayList<List<?>>();
  129. }
  130.  
  131. List<?> row = getRow( resultSet );
  132.  
  133. list.add( row );
  134. }
  135.  
  136. }
  137. catch( SQLException e ) {
  138.  
  139. throw e;
  140. }
  141. catch( Exception e ) {
  142.  
  143. throw( new SQLException( "Error executing query: " + sql, e ) );
  144. }
  145. finally {
  146.  
  147. if( statement != null ) {
  148. statement.close();
  149. }
  150.  
  151. if( resultSet != null ) {
  152. resultSet.close();
  153. }
  154.  
  155. if( connection != null ) {
  156. connection.close();
  157. }
  158. }
  159.  
  160. return list;
  161. }
  162.  
  163.  
  164. /**
  165. * Executes the given SQL statement. May be an INSERT, UPDATE, or DELETE statement
  166. * or any SQL statement that returns no value.
  167. *
  168. * @param sql
  169. * @return
  170. * the number of rows updated by statement
  171. *
  172. * @throws SQLException
  173. */
  174. public int execute( String sql ) throws SQLException {
  175.  
  176. if( log.isDebugEnabled() ) {
  177. log.debug( sql );
  178. }
  179.  
  180. Connection connection = null;
  181. Statement statement = null;
  182.  
  183. int updated = 0;
  184.  
  185. try {
  186.  
  187. connection = getConnection();
  188. statement = connection.createStatement();
  189. updated = statement.executeUpdate( sql );
  190. }
  191.  
  192. finally {
  193.  
  194. if( statement != null ) {
  195. statement.close();
  196. }
  197.  
  198. if( connection != null ) {
  199. connection.close();
  200. }
  201. }
  202.  
  203. return updated;
  204. }
  205.  
  206.  
  207.  
  208. /**
  209. * Update DB rows corresponding to argument values. Method will generate relevant SQL
  210. * and prepared statements given table meta-data arguments and will execute in batches
  211. * if number of values exceeds batch threshold size.
  212. *
  213. * @param values
  214. * @param table
  215. * @param columns
  216. * @param primaryKey
  217. * @param batchSize
  218. * @throws SQLException
  219. */
  220. @SuppressWarnings({ "rawtypes", "unchecked" })
  221. public void update( List<List<?>> values, String table, List<String> columns, String primaryKey, int batchSize ) throws SQLException {
  222.  
  223. StringBuffer sb = new StringBuffer();
  224. StringBuffer where = new StringBuffer();
  225. sb.append("UPDATE ").append( table ).append(" SET ");
  226.  
  227. int primaryIndex = -1;
  228.  
  229. int i = 0;
  230. for( String column : columns ) {
  231.  
  232. if( i > 0 ) {
  233. sb.append(",");
  234. }
  235.  
  236.  
  237. sb.append( column ).append( "=" ).append("?");
  238.  
  239. if( column.equalsIgnoreCase( primaryKey ) ) {
  240.  
  241. where.append(" WHERE ").append( column ).append( "=" ).append( "?" );
  242. primaryIndex = i;
  243. }
  244.  
  245. i++;
  246. }
  247.  
  248. sb.append( where.toString() );
  249.  
  250. String sql = sb.toString();
  251.  
  252.  
  253. // in order to update value where PRIMARY_KEY = primaryKeyValue
  254. // we need to augment the value set so that this can occur dynamically
  255. // in context of prepared statement update
  256. // (append primary key value to end of list so that it appears in
  257. // correct place relative to prepared statement where clause)
  258. for( List row : values ) {
  259.  
  260. Object primaryValue = row.get( primaryIndex );
  261. row.add( primaryValue );
  262. }
  263.  
  264. update( values, sql, batchSize );
  265. }
  266.  
  267.  
  268. /**
  269. * Retrieve NEXT ID for given sequence.
  270. *
  271. * (NOTE: default implementation is compatible with all DBs)
  272. *
  273. * @param sequence
  274. * @return
  275. * @throws SQLException
  276. */
  277. public Integer nextId( String sequence ) throws SQLException {
  278.  
  279. try {
  280.  
  281. if( sequence.toUpperCase().endsWith( ".NEXTVAL" ) == false ) {
  282. sequence = sequence + ".NEXTVAL";
  283. }
  284.  
  285. List<List<?>> results = select( "select " + sequence + " from dual" );
  286.  
  287. BigDecimal id = (BigDecimal)results.get( 0 ).get( 0 );
  288.  
  289. Integer nextId = Integer.valueOf( id.intValue() );
  290. return nextId;
  291. }
  292. catch( SQLException e ) {
  293.  
  294. throw e;
  295. }
  296. catch( Exception e ) {
  297.  
  298. throw( new SQLException("Error getting next ID for sequence: " + sequence ) );
  299. }
  300.  
  301. }
  302.  
  303.  
  304.  
  305.  
  306. /**
  307. * Performs a batch update using the argument sql string as a prepared statement against the argument values.
  308. *
  309. * Batch updates are transactional, however it is important to note that very large batches may
  310. * incur out of memory errors as the batch prepared statement list grows very large. Commit is required to
  311. * flush batch prior to it growing to unmanagable sizes. It is left to the caller to determine batch "size"
  312. * by limiting the argument value list size to an acceptable value.
  313. *
  314. * Finding optimal batch size may require some experimentation. Larger batches should result in more optimal
  315. * speed performance, but may result in OutOfMemoryErrors if the batch is too large. This issue may even occur
  316. * if multiple successful batches are executed in sequence.
  317. *
  318. * NOTE: In development of this functionality I was attempting to load a dataset with approximately 800,000 records.
  319. * I found that queuing batches of 10,000 would work successfully, but would eventually run OOM after processing
  320. * 500,000+ records as it appeared the memory required for the prepared statement batching would outrun
  321. * garbage collection. Ultimately went with batches of 1000 which seemed to offer the ultimate performance balance.
  322. *
  323. * @param values
  324. * @param sql
  325. *
  326. * @throws SQLException
  327. */
  328. public void update( List<List<?>> values, String sql ) throws SQLException {
  329.  
  330. if( log.isDebugEnabled() ) {
  331. log.debug( sql );
  332. }
  333.  
  334. // INSERT INTO CUSTOMERS VALUES (?,?,?)
  335. // statement.setInt(1,1);
  336. // statement.setString(2, "J Smith");
  337. // statement.setString(3, "617 555-1323");
  338.  
  339. Connection connection = null;
  340. PreparedStatement statement = null;
  341.  
  342. try {
  343.  
  344. connection = getConnection();
  345. connection.setAutoCommit( false ); // allow for transactional batch rollback
  346. statement = connection.prepareStatement( sql );
  347.  
  348. // each row should represent a insert statement
  349. for( List<?> row : values ) {
  350.  
  351. for( int i = 0; i < row.size(); i++ ) {
  352.  
  353. Object value = row.get( i );
  354. setStatementValue( statement, i+1, value ); // sql index starts at 1 so offset accordingly
  355. }
  356.  
  357. statement.addBatch();
  358. }
  359.  
  360. long t0 = System.currentTimeMillis();
  361.  
  362. @SuppressWarnings("unused")
  363. int[] updateCounts = statement.executeBatch();
  364. connection.commit();
  365. statement.clearBatch(); // not sure if this is necessary
  366.  
  367. long t1 = System.currentTimeMillis();
  368.  
  369. if( log.isDebugEnabled() ) {
  370. String elapsedSeconds = ( new DecimalFormat( "#.##" ).format( ((float) (t1-t0) / 1000) ) );
  371. log.debug( values.size() + " records batch inserted (" + elapsedSeconds + "s) " );
  372. }
  373. }
  374. catch ( Exception e ) {
  375.  
  376. try {
  377. connection.rollback(); // if failure occurs then rollback transaction
  378. }
  379. catch ( Exception e2 ) {
  380.  
  381. throw ( new SQLException( "Error attempting to rollback batch insert", e2 ) );
  382. }
  383.  
  384. throw ( new SQLException( "Error execting batch insert", e ) );
  385. }
  386. finally {
  387.  
  388. if( statement != null ) {
  389. statement.close();
  390. }
  391.  
  392. if( connection != null ) {
  393. connection.close();
  394. }
  395. }
  396. }
  397.  
  398.  
  399. /**
  400. * Insert values is sub-batches of "threshold" size.
  401. *
  402. * @param values
  403. * @param sql
  404. * @param threshold
  405. * @throws SQLException
  406. */
  407. public void update( List<List<?>> values, String sql, int batchSize ) throws SQLException {
  408.  
  409. if( batchSize < 0 || batchSize <= values.size() ) {
  410.  
  411. update( values, sql );
  412. return;
  413. }
  414.  
  415. try {
  416.  
  417. int threshold = getBatchThreshold();
  418.  
  419. if( log.isDebugEnabled() ) {
  420. log.debug( "Inserting " + values.size() + " values in batches of " + threshold );
  421. }
  422.  
  423. long t0 = System.currentTimeMillis();
  424.  
  425.  
  426. List<List<?>> batch = new ArrayList<List<?>>( threshold );
  427.  
  428. int count = 0;
  429. for( List<?> row : values ){
  430.  
  431. batch.add( row );
  432. count++;
  433.  
  434. // execute in batches to scale performance and avoid running out of memory
  435. if( batch.size() % threshold == 0 ) {
  436.  
  437. update( batch, sql );
  438. batch.clear();
  439.  
  440. if( log.isDebugEnabled() ) {
  441. log.debug( ((int) Math.floor( count * 100 / values.size() )) + "%" );
  442. }
  443. }
  444.  
  445. }
  446.  
  447. // modulus batch
  448. if( batch.isEmpty() == false ) {
  449. //jdbcBroker.insertBatch( sql, values );
  450. update( batch, sql );
  451. batch = null;
  452. }
  453.  
  454. // commit transaction
  455. //connection.commit();
  456.  
  457. long t1 = System.currentTimeMillis();
  458.  
  459. if( log.isDebugEnabled() ) {
  460. String elapsedSeconds = ( new DecimalFormat( "#.##" ).format( ((float) (t1-t0) / 1000) ) );
  461. log.debug( values.size() + " records batch inserted (" + elapsedSeconds + "s) " );
  462. }
  463. }
  464. catch( Exception e ) {
  465.  
  466. throw ( new SQLException( "Error execting batch insert", e ) );
  467. }
  468. }
  469.  
  470.  
  471.  
  472. /**
  473. * Retrieve prepared statement SQL for batch insert.
  474. * Default implementation assumes a sequence is defined.
  475. */
  476. public void insert( List<List<?>> values, String table, List<String> columns, String primaryKey, String sequence, int batchSize ) throws SQLException {
  477.  
  478. StringBuffer sb = new StringBuffer();
  479. StringBuffer prep = new StringBuffer();
  480. sb.append("INSERT INTO ").append( table );
  481. sb.append(" (" );
  482.  
  483. boolean first = true;
  484. for( String column : columns ) {
  485.  
  486.  
  487. if( first == false ) {
  488. sb.append(",");
  489. prep.append(",");
  490. }
  491.  
  492. sb.append( column );
  493. if( sequence != null && column.equalsIgnoreCase( primaryKey ) ) {
  494.  
  495. prep.append( sequence ); // insert assumes sequence implementation
  496. }
  497. else {
  498. prep.append("?");
  499. }
  500.  
  501. first = false;
  502. }
  503.  
  504. sb.append(") VALUES (").append( prep.toString() ).append(")");
  505.  
  506. String sql = sb.toString();
  507.  
  508. update( values, sql, batchSize ); // use batching by default
  509. }
  510.  
  511.  
  512. /**
  513. * Converts column name list to a select clause, null is interpreted as all columns '*'
  514. *
  515. * If a prefix is specified then prepend then it will be prepended to each column name
  516. *
  517. * @param columns
  518. * @param prefix
  519. * @return
  520. */
  521. protected String getSelectClause( List<String> columns, String prefix ) {
  522.  
  523. if( prefix == null ) {
  524. prefix = "";
  525. }
  526.  
  527. if( prefix.length() > 0 && prefix.endsWith(".") == false ) {
  528. prefix += ".";
  529. }
  530.  
  531. if( columns == null || columns.size() == 0 ) {
  532.  
  533. return prefix + "*";
  534. }
  535.  
  536. StringBuffer clause = new StringBuffer();
  537. boolean first = true;
  538. for( String column : columns ) {
  539.  
  540. if( first == false ) {
  541. clause.append( ", " );
  542. }
  543.  
  544. clause.append( prefix ).append( column );
  545. first = false;
  546. }
  547.  
  548. return clause.toString();
  549. }
  550.  
  551.  
  552. @SuppressWarnings("rawtypes")
  553. protected void setStatementValue( PreparedStatement statement, int index, Object value ) throws SQLException {
  554.  
  555. try {
  556.  
  557. if( value == null ) {
  558. statement.setString( index, null );
  559. }
  560. else if( value instanceof String ) {
  561. statement.setString( index, value.toString() );
  562. }
  563. else if( value instanceof Boolean ) {
  564. statement.setBoolean( index, ((Boolean) value).booleanValue() );
  565. }
  566. else if( value instanceof Long ) {
  567. statement.setLong( index, ((Long) value).longValue());
  568. }
  569. else if( value instanceof Integer ) {
  570. statement.setInt( index, ((Integer) value).intValue());
  571. }
  572. else if( value instanceof Double ) {
  573. statement.setDouble( index, ((Double) value).doubleValue());
  574. }
  575. else if( value instanceof Date ) {
  576. statement.setTimestamp( index, new Timestamp( ((Date) value).getTime() ) );
  577. }
  578. else if( value instanceof Timestamp ) {
  579. statement.setTimestamp( index, (Timestamp) value );
  580. }
  581. else if( value instanceof List ) {
  582.  
  583. // list is expected for an IN expression
  584. List list = (List) value;
  585. Iterator iterator = list.iterator();
  586.  
  587. for( int i = index; iterator.hasNext(); i++ ) {
  588.  
  589. Object o = iterator.next();
  590. setStatementValue( statement, i, o );
  591. }
  592. }
  593. else {
  594.  
  595. throw( new SQLException("Value of type " + value.getClass().getName() + "' is not supported.") );
  596. }
  597. }
  598. catch( SQLException e ) {
  599.  
  600. throw e;
  601. }
  602. catch( Exception e ) {
  603.  
  604. throw( new SQLException( "Error setting statement value.", e ) );
  605. }
  606.  
  607. }
  608.  
  609.  
  610. @SuppressWarnings({ "rawtypes", "unchecked" })
  611. protected List getRow( ResultSet rs ) throws SQLException {
  612.  
  613. List row = null;
  614.  
  615. try {
  616.  
  617. ResultSetMetaData meta = rs.getMetaData();
  618.  
  619. int columns = meta.getColumnCount();
  620.  
  621. row = new ArrayList( columns );
  622.  
  623. for( int i = 1; i <= columns; i++ ) {
  624.  
  625. int type = meta.getColumnType( i );
  626.  
  627. Object value = null;
  628. if( Types.NULL == type ) {
  629. value = null;
  630. }
  631. else if( Types.VARCHAR == type ) {
  632. value = rs.getString( i );
  633. }
  634. else if( Types.INTEGER == type ) {
  635. value = Integer.valueOf( rs.getInt( i ) );
  636. }
  637. else if( Types.BIGINT == type ) {
  638. value = Long.valueOf( rs.getLong( i ) );
  639. }
  640. else if( Types.DOUBLE == type ) {
  641. value = rs.getDouble( i );
  642. }
  643. else if( Types.TIMESTAMP == type ) {
  644. value = rs.getTimestamp( i );
  645. }
  646. else if( Types.BOOLEAN == type ) {
  647. value = rs.getBoolean( i );
  648. }
  649. else if( Types.NUMERIC == type ) {
  650. value = rs.getBigDecimal( i );
  651. }
  652. else {
  653.  
  654. throw( new SQLException( "Unhandled sql type: " + type ) );
  655. }
  656.  
  657. row.add( value );
  658. }
  659. }
  660. catch( SQLException e ) {
  661. throw e;
  662. }
  663. catch( Exception e ) {
  664. throw( new SQLException( "Error building persisted object", e ) );
  665. }
  666.  
  667. return row;
  668. }
  669.  
  670.  
  671. /**
  672. * Return value as valid SQL. Implmentation assumes toString() implementation for value class
  673. * @param value
  674. * @return
  675. */
  676. public String toSql( Object value ) throws EpocratesException {
  677.  
  678. if( value == null ) {
  679.  
  680. return "null";
  681. }
  682.  
  683. if( value instanceof Integer ) {
  684.  
  685. return( value.toString() );
  686. }
  687. else if( value instanceof Double ) {
  688.  
  689. return( value.toString() );
  690. }
  691. else if( value instanceof String ) {
  692.  
  693. return( "\'" + value.toString() + "\'" );
  694. }
  695. else {
  696.  
  697. throw( new ValidationException( "Value of type " + value.getClass().getName() + " could not be converted to SQL") );
  698. }
  699. }
  700.  
  701.  
  702. /**
  703. * This implementation accomodates the need to state an explicit sort order by value.
  704. *
  705. * For exmaple, we have an IN statement:
  706. * WHERE id IN( 5,2,6,8,12,1 )
  707. * and we want the results to match this order using the following order by clause
  708. * ORDER BY DECODE(id,5,1,2,2,6,3,8,4,12,5,1,6);
  709. *
  710. * @param query
  711. * @return
  712. */
  713. public String getIndexedSort( String field, List<?> values ) {
  714.  
  715. StringBuilder sb = new StringBuilder();
  716. sb.append("DECODE(");
  717. sb.append( field );
  718.  
  719. int i = 1;
  720. for( Object o : values ) {
  721.  
  722. sb.append(",").append( o ).append(",").append( i );
  723. i++;
  724. }
  725.  
  726. sb.append(")");
  727.  
  728. return sb.toString();
  729. }
  730.  
  731.  
  732. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement