diff -r 000000000000 -r 3060119b1292 classes/hirondelle/web4j/database/SqlFetcher.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/classes/hirondelle/web4j/database/SqlFetcher.java Wed Dec 04 17:00:31 2013 +0100 @@ -0,0 +1,359 @@ +package hirondelle.web4j.database; + +import hirondelle.web4j.BuildImpl; +import hirondelle.web4j.readconfig.Config; +import hirondelle.web4j.util.Args; +import hirondelle.web4j.util.Consts; +import hirondelle.web4j.util.Util; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.logging.Logger; + +/** + Perform a single SELECT command. + +

To perform single INSERT, DELETE, or UPDATE operation, use {@link SqlEditor} instead. + +

The utilities in the {@link Db} class should always + be considered as a simpler alternative to this class. + +

The user of this class is able to fetch records in a very + compact style, without concern for details regarding +

+

Example use case in a Data Access Object(DAO), where a Person object +is fetched using aName as a simple business identifier : +

+Person fetch(String aName) throws DAOException {
+  Args.checkForContent(aName);
+  SqlFetcher3 fetcher = SqlFetcher3.forSingleOp(FETCH_PERSON, Util.asList(aName));
+  ModelBuilder3 builder = new ModelFromRow(Person.class);
+  return (Person)fetcher.fetchObject( builder );
+}
+
+

Note that the above feature is implemented using only +

+ Given these database utility classes, many DAO implementations + become very compact. (The {@link Db} class can reduce the size of + implementations even further.) + +

Almost all callers of SqlFetcher will also use a {@link ModelBuilder}, + which is closely related to this class. + +

SqlFetcher can participate in a transaction by passing a + {@link Connection} to one of its static factory methods. Otherwise, an internal + Connection is obtained from {@link hirondelle.web4j.database.ConnectionSource}, + using {@link SqlId#getDatabaseName()} to identify the database. +*/ +final class SqlFetcher { + + /** + Factory method for single SELECT operation which takes parameters. + */ + static SqlFetcher forSingleOp(SqlId aSqlId, Object... aParams){ + return new SqlFetcher(aSqlId, INTERNAL_CONNECTION, NO_SEARCH_CRITERIA, aParams); + } + + /** + Factory method for SELECT operation which takes parameters and participates in + a transaction. + + @param aConnection used by all operations participating in the given transaction; + this connection is owned by the caller. + */ + static SqlFetcher forTx(SqlId aSqlId, Connection aConnection, Object... aParams){ + return new SqlFetcher(aSqlId, aConnection, NO_SEARCH_CRITERIA, aParams); + } + + /** + Factory method for a SELECT operation which takes parameters, and whose criteria are dynamic. + */ + static SqlFetcher forSearch(SqlId aSqlId, DynamicSql aSearchCriteria, Object... aParams) { + return new SqlFetcher(aSqlId, INTERNAL_CONNECTION, aSearchCriteria, aParams); + } + + /** + Perform a SELECT and return the first row as a Model Object. + +

Intended for use with SELECT statements which usually return one row.
+ If 1..N rows are returned, parse the first row into a Model Object and + return it. Ignore any other rows.
If no rows are returned, return null. + (This behavior can occasionally be used to limit the result set to a single record. + It is a convenient alternative when the database has no robust or simple method of + limiting the number of returned rows. However, this technique should be used + only if the number of returned rows is not excessively large.) + + @param aModelBuilder parses a row into a corresponding Model Object. + */ + T fetchObject(ModelBuilder aModelBuilder) throws DAOException { + List list = new ArrayList(); + fetchObjects(aModelBuilder, list); + return list.isEmpty()? null : list.get(0); + } + + /** + Perform a SELECT and return a corresponding Collection of Model Objects. + +

Intended for use with SELECT statements which usually return multiple rows.
+ If 1..N rows are found, parse each row into a Model Object and + place it in aResult.
Items are added to aResult + in the order returned from the underlying ResultSet.
+ If 0 rows are found, then aResult will remain empty. + + @param aModelBuilder parses a row into a corresponding Model Object. + @param aResult acts as an "out" parameter, and is initially empty. + */ + void fetchObjects(ModelBuilder aModelBuilder, Collection aResult) throws DAOException { + checkIsEmpty(aResult); + + ResultSet resultSet = fetchRows(); + RowCycler cycler = new CollectionRowCycler(aResult); + cycler.cycleRows(resultSet, aModelBuilder); + if (aResult.size() == 0){ + fLogger.fine("No Results Found for query: " + fSql); + } + } + + /** + Set parameters for limiting returned items to a specific block of rows. + +

This method is not usually called. It is intended only for those + cases in which only a subset of the underlying ResultSet is to be + returned. Usually, all rows of the underlying result set are of interest, + and none should be eliminated from processing. + + @param aStartIdx 1 or more + @param aPageSize 1 or more, the number of items returned to the caller + */ + void limitRowsToRange(Integer aStartIdx, Integer aPageSize){ + Args.checkForPositive(aStartIdx.intValue()); + Args.checkForPositive(aPageSize.intValue()); + fStartIdx = aStartIdx; + fEndIdx = getEndIdx(aStartIdx, aPageSize); + } + + /** + Override the default transaction isolation level specified in web.xml. + +

This setting is applied only if this class is using its own internal connection, and + has not received a connection from the caller. + +

If the user passed an external + Connection to a constructor of this class, then this method + must not be called. (Changing the isolation level after a transaction + has started is not permitted - see {@link Connection}). + + @see TxIsolationLevel + */ + void setTxIsolationLevel(TxIsolationLevel aTxIsolationLevel) { + if ( ! fConnectionIsInternal ) { + throw new IllegalStateException ( + "Cannot set transaction isolation level after transaction has started." + ); + } + fLogger.fine("Setting transaction isolation level to " + aTxIsolationLevel); + fExplicitTxIsolationLevel = aTxIsolationLevel; + } + + // PRIVATE + + private SqlStatement fSql; + private PreparedStatement fStatement; + + private Connection fConnection; + private boolean fConnectionIsInternal; + /** + Explicit Isolation level, for internal connections only. + If null, then the isolation level defined by {@link Config} for the given db name will be used. + */ + private TxIsolationLevel fExplicitTxIsolationLevel; + + private Integer fStartIdx; + private Integer fEndIdx; + + private static final Connection INTERNAL_CONNECTION = null; + private static final DynamicSql NO_SEARCH_CRITERIA = null; + + private static final Logger fLogger = Util.getLogger(SqlFetcher.class); + + private SqlFetcher(SqlId aSqlId, Connection aConnection, DynamicSql aSearchCriteria, Object... aParams){ + fSql = new SqlStatement(aSqlId, aSearchCriteria, aParams); + if ( aConnection != null ) { + fConnectionIsInternal = false; + fConnection = aConnection; + } + else { + fConnectionIsInternal = true; + } + } + + private ResultSet fetchRows() throws DAOException { + ResultSet result = null; + Connection connection = null; + try { + connection = getConnection(); + fStatement = fSql.getPreparedStatement(connection); + result = fStatement.executeQuery(); + DbUtil.logWarnings(fStatement); + } + catch (SQLException rootCause){ + String message = + "Cannot execute fetch: " + rootCause + Consts.SPACE + rootCause.getMessage() + + Consts.SPACE + fSql + ; + fLogger.severe(message); + //unusual: the close is not in a finally block, since it must only be closed if there's a problem + close(fStatement, connection); + throw new DAOException(message, rootCause); + } + return result; + } + + /** + Cycles over a ResultSet, but what is done to each row is left + up to subclasses. + + (This private class exists in order to eliminate code repetition between the + Collection and Map styles used above.) + */ + private abstract class RowCycler { + void cycleRows(ResultSet aResultSet, ModelBuilder aModelBuilder) throws DAOException { + try { + while (aResultSet.next()){ + if ( ! isLimitingRows() ) { + addItemToResult(aModelBuilder, aResultSet); + } + else { + if ( isAfterStart(aResultSet) ){ + if ( ! isAfterEnd(aResultSet) ) { + addItemToResult(aModelBuilder, aResultSet); + } + else { + fLogger.fine( + "Finished processing ResultSet. This row is passed the end : " + + aResultSet.getRow() + ); + break; + } + } + else { + fLogger.fine( + "Skipping row. Index not yet high enough to start processing : " + + aResultSet.getRow() + ); + } + } + } + } + catch (SQLException rootCause){ + String message = + "Cannot execute fetch: " + rootCause + Consts.SPACE + + rootCause.getErrorCode() + Consts.SPACE + fSql + ; + throw new DAOException(message, rootCause); + } + finally { + close(fStatement, fConnection); + } + } + abstract void addItemToResult(ModelBuilder aModelBuilder, ResultSet aResultSet) throws DAOException, SQLException; + } + + private final class CollectionRowCycler extends RowCycler { + CollectionRowCycler(Collection aCollection){ + fCollection = aCollection; + } + void addItemToResult(ModelBuilder aModelBuilder, ResultSet aResultSet) throws DAOException { + T item = aModelBuilder.buildObject(aResultSet); + fCollection.add( item ); + } + private Collection fCollection; + } + + private void checkIsEmpty(Collection aOutParam){ + if ( ! aOutParam.isEmpty() ) { + throw new IllegalArgumentException("Out parameter is not initially empty."); + } + } + + /** + Return either the external connection, passed by the caller, or an internal one with + the appropriate isolation level. + */ + private Connection getConnection() throws DAOException { + if ( fConnectionIsInternal ) { + String dbName = fSql.getSqlId().getDatabaseName(); + if( Util.textHasContent(dbName) ){ + fConnection = BuildImpl.forConnectionSource().getConnection(dbName); + } + else{ + fConnection = BuildImpl.forConnectionSource().getConnection(); + } + TxIsolationLevel.set(getIsolationLevel(dbName), fConnection); + } + else { + //use the external connection passed to the constructor + } + return fConnection; + } + + private TxIsolationLevel getIsolationLevel(String aDbName){ + TxIsolationLevel result = null; + if(fExplicitTxIsolationLevel != null) { + result = fExplicitTxIsolationLevel; + } + else { + result = new Config().getSqlFetcherDefaultTxIsolationLevel(aDbName); + } + return result; + } + + private void close( + PreparedStatement aStatement, Connection aConnection + ) throws DAOException { + if ( fConnectionIsInternal ) { + DbUtil.close(aStatement, aConnection); + } + else { + //do not close the connection, since it is owned by the caller, and is + //needed by other statements in the transaction + DbUtil.close(aStatement); + } + } + + private Integer getEndIdx(Integer aStartIdx, Integer aPageSize){ + return new Integer( aStartIdx.intValue() + aPageSize.intValue() - 1); + } + + private boolean isLimitingRows() { + return fStartIdx != null; + } + + private boolean isAfterStart(ResultSet aResultSet) throws SQLException { + return aResultSet.getRow() >= fStartIdx.intValue(); + } + + private boolean isAfterEnd(ResultSet aResultSet) throws SQLException { + return aResultSet.getRow() > fEndIdx.intValue(); + } +}