//============================================================================== // SqlObjectQuery.java //============================================================================== package tribble.sql; import java.lang.Character; import java.lang.Exception; import java.lang.NoSuchMethodException; import java.lang.NullPointerException; import java.lang.String; import java.lang.StringBuffer; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; /******************************************************************************* * SQL query builder. * Uses a JDBC connection to query an SQL database, then stores the results of * the query into an object of a class type provided by the client. * * * *

Usage

*
* *

* Our client has a JDBC connection to a database: *

*    Connection  conn;
*    Statement   stmt;
*
*    // Establish the JDBC connection
*    conn = driverManager.getConnection(...);
*    stmt = conn.createStatement(...);
* *

* The following SQL query is to be performed against the database: *

*    SELECT TOP 100 DISTINCT
*      e.first_name,
*      e.last_name,
*      e.employee_id,
*      w.salary_per_year
*    FROM employees e
*    JOIN employee_wages w
*      ON w.employee_id = e.employee_id
*    WHERE e.last_name BETWEEN 'R' AND 'S'
*    ORDER BY e.last_name
* *

* Each row of the result of this query is to be assigned to an object of type * EmployeeInfo: *

*    public class EmployeeInfo
*    {
*        public EmployeeInfo() { ... }
*
*        public void setId(int id) { ... }
*        public void setFirstName(String name) { ... }
*        public void setLastName(String name) { ... }
*        public void setSalary(double amt) { ... }
*        ...
*    }
* *

* Note that the EmployeeInfo result type must be public, and have * public setter methods and/or public member variables. * *

* We construct a {@link SqlObjectQuery} object and provide it with the * connection and the query. Note that the SELECT keyword of the query * is omitted; we provide any options follwing the implied SELECT * keyword by calling {@link #addSelect addSelect()}, and we provide the selected * column names by calling {@link #addField addField()}. The rest of the query, * starting with the FROM clause, is provided by calling * {@link #addClause addClause()}: * *

*    SqlObjectQuery  req;
*
*    // Build the SQL query and result object mapping
*    req = new {@link #SqlObjectQuery SqlObjectQuery}(EmployeeInfo.class);
*
*    req.{@link #addSelect addSelect}("TOP 100");
*    req.addSelect("DISTINCT");
*
*    req.{@link #addField addField}("e.employee_id", "setId()");
*    req.addField("e.first_name", "setFirstName()");
*    req.addField("e.last_name", "setLastName()");
*    req.addField("w.salary_per_year", "setSalary()");
*
*    req.{@link #addClause addClause}("FROM employees e");
*    req.addClause("JOIN employee_wages w ON w.employee_id = e.employee_id");
*    req.addClause("WHERE e.last_name BETWEEN 'R' AND 'S'");
*    req.addClause("ORDER BY e.last_name");
*
*    // e.employee_id     will be set by EmployeeInfo.setId()
*    // e.first_name      will be set by EmployeeInfo.setFirstName()
*    // e.last_name       will be set by EmployeeInfo.setLastName()
*    // w.salary_per_year will be set by EmployeeInfo.setSalary()
* *

* Each invocation of addField() assigns a field (column) of the SQL * query result to either a class setter method or a class member variable in the * result object type. The first argument specifies the text of the field to be * included in the SQL query string. The second argument specifies the name of * either a setter method (by ending with "()") or the name of a member * variable. In either case, the named member must be public. Java reflection * is used to locate the specified class member. (Note that setting a member * variable directly is generally considered bad object-oriented programming * style, the use of a setter method being considered safer, but such capability * is included here for completeness.) * *

* If a query field is needed for the query string itself but the value is not * used in the result object (such as a field used for a GROUP * BY clause), null is passed as the second argument. The field will be * included in the SQL query string, but the resulting value for it will be * ignored. * *

* Note that the order of the invocations to addField() is important; * the first call assigns the result of column 1, the second call assigns the * result of column 2, and so forth. This means that calls to * addSelect() must not specify additional query fields; otherwise, the * resulting SQL query result member assignments will not behave as expected. * *

* Once the query has been set up, it is then executed: *

*    {@link SqlObjectResultSet}  rset = null;
*
*    try
*    {
*        // Execute the SQL query
*        rset = req.{@link #executeQuery executeQuery}(stmt);
*    }
*    catch (SQLException ex)
*    {
*        // Failure
*        System.out.println("SQL query failed");
*        throw ex;
*    }
*    finally
*    {
*        // Clean up
*        if (rset != null)
*            rset.{@link SqlObjectResultSet#close close}();
*        if (req != null)
*            req.{@link #close close}();
*        if (stmt != null)
*            stmt.close();
*    }
* *

* The results of the query (zero or more rows) can then be retrieved in a way * similar to the way results are obtained from a JDBC {@link ResultSet}, one row * at a time: * *

*    // Retrieve the results of the SQL query
*    while (rset.next())
*    {
*        EmployeeInfo    row;
*
*        // Get the next query result row as an object
*        row = (EmployeeInfo) rset.{@link SqlObjectResultSet#getResult getResult}();
*
*        // e.employee_id     is set by row.setId()
*        // e.first_name      is set by row.setFirstName()
*        // e.last_name       is set by row.setLastName()
*        // w.salary_per_year is set by row.setSalary()
*
*        System.out.println("Employee: " + row.getFirstName() + " "
*            + row.getLastName());
*        System.out.println("  ID #" + row.getId() + ":  $" + row.getSalary());
*    }
*
*    // Clean up
*    rset.{@link SqlObjectResultSet#close close}();
*    req.{@link #close close}();
*    stmt.close();
* *

* The call to getResult() instantiates a new result object, in this * case of type EmployeeInfo, and populates its members from the query * result according to the order in which the addField() calls were made * above. The result is a single object containing members whose values are * retrieved from columns of a row of the query result. * *

* For increased efficiency, caching of the result class information can be * enabled. This causes information about the result class type to be stored in * a local cache each time an {@link SqlObjectQuery} object is instantiated. * This can speed up processing if the same result class types are used multiple * times. Caching is disabled by default, but can be enabled by calling * {@link #enableCaching enableCaching()}. Information about a specific result * class can also be cached explicitly by calling {@link #cacheInfo cacheInfo()}. * Note that the cache is static, so it is shared by all {@link SqlObjectQuery} * instances within the same JVM. * * *

*
Source code:
*
Available at: * http://david.tribble.com/src/java/tribble/sql/SqlObjectQuery.java *
*
Documentation:
*
Available at: * http://david.tribble.com/docs/tribble/sql/SqlObjectQuery.html *
*
* * * @version @(#)$Revision: 1.20 $ $Date: 2008/09/15 17:38:43 $ * @since 2008-02-14 * @author David R. Tribble (david@tribble.com) *

* Copyright ©2008 by David R. Tribble, all rights reserved.
* Permission is granted to any person or entity except those designated by * by the United States Department of State as a terrorist, or terrorist * government or agency, to use and distribute this source code provided * that the original copyright notice remains present and unaltered. */ public class SqlObjectQuery { static final String REV = "@(#)tribble/sql/SqlObjectQuery.java $Revision: 1.20 $ $Date: 2008/09/15 17:38:43 $\n"; // ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ // Static variables /** Cache for storing result type class info. */ private static HashMap/**/ s_cache; /** Store result type class info in a cache. */ private static boolean s_infoIsCached; // ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ // Static methods /*************************************************************************** * Enable or disable caching of result type class information. * This can speed up processing if the same result type classes are re-used * multiple times. * * @param flag * If true, information about result type classes is cached, otherwise * caching is disabled. * The cache is static, so it is shared by all {@link SqlObjectQuery} * instances within the same JVM. * * @return * The previous cache setting. * * @see #cacheInfo cacheInfo() * * @since 1.19, 2008-09-07 */ public static boolean enableCaching(boolean flag) { boolean prev; // Set the class info caching flag prev = s_infoIsCached; s_infoIsCached = flag; return prev; } // ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ // Variables /** Query result object type. */ Class m_type; /** Constructor (default, no-arg) for the result object type. */ private Constructor m_ctor; /** Methods for the result object class. */ private HashMap/**/ m_methods; /** List of field (table column) names to SELECT in the SQL query. */ private ArrayList/**/ m_fields = new ArrayList/**/(10); /** List of mappings from the selected fields to result class members. */ private ArrayList/**/ m_mappings = new ArrayList/**/(10); /** SQL SELECT clause parameters. */ private StringBuffer m_select; /** SQL query clauses (starting with the FROM clause). */ private StringBuffer m_clauses; /** SQL query to perform. */ private String m_query; // ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ // Constructors /*************************************************************************** * Constructor. * * @param resultType * Class type of the result of the SQL query. * Each row of the result set will be converted into an object of this type. * * @throws SQLException * Thrown if the result class type is not public or is abstract, or does not * have a public default (no-arg) constructor. * * @since 1.1, 2008-02-14 */ public SqlObjectQuery(Class resultType) throws SQLException { // Initialize setResultType(resultType); } // ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ // Methods /*************************************************************************** * Establish the object type into which the query results will be extracted. * * @param type * Specifies the class type to which the SQL query results are to be * assigned. * * @throws SQLException * Thrown if the result class type is not public or is abstract, or does not * have a public default (no-args) constructor. * * @since 1.1, 2008-02-14 */ private void setResultType(Class type) throws SQLException { String typeName; Field[] vars; Method[] methods; int mods; HashMap list; Iterator it; // Retrieve info for the result class type m_type = type; typeName = type.getName(); // Check for cached info about the class type if (s_cache != null) { Object[] info; info = (Object[]) s_cache.get(type); if (info != null) { m_ctor = (Constructor) info[0]; m_methods = (HashMap/**/) info[1]; return; } } // Check the accessibility of the result class mods = type.getModifiers(); if ((mods & Modifier.PUBLIC) == 0) throw new SQLException("Result type is not public: " + typeName); if ((mods & Modifier.ABSTRACT) != 0) throw new SQLException("Result type is abstract: " + typeName); // Get the default (no-arg) public constructor for the result class try { m_ctor = type.getConstructor(new Class[0]); } catch (Exception ex) { throw new SQLException("Missing or inaccessible public default" + " constructor for result type: " + typeName); } // Get all of the candidate setter methods of the result class methods = type.getDeclaredMethods(); list = new HashMap(methods.length * 4 / 3); for (int i = 0; i < methods.length; i++) { Method meth; Class[] parmTypes; String name; // See if the class method is a compatibly callable setter meth = methods[i]; mods = meth.getModifiers(); if ((mods & Modifier.ABSTRACT) != 0 || (mods & Modifier.STATIC) != 0 || (mods & Modifier.PUBLIC) == 0) continue; // Verify that the method has a single compatible parameter parmTypes = meth.getParameterTypes(); if (parmTypes.length != 1) continue; if (FieldTypeMap.getInstance(parmTypes[0]) == null) continue; // Add the method to the list of setters, checking for duplicates name = meth.getName(); if (list.containsKey(name)) list.put(name, null); else list.put(name, meth); } // Copy the list of setter methods, removing duplicates m_methods = new HashMap/**/(list.size() * 4 / 3); it = list.keySet().iterator(); while (it.hasNext()) { String name; Method meth; name = (String) it.next(); meth = (Method) list.get(name); if (meth != null) m_methods.put(name, meth); } // Cache the class type info, if enabled if (s_infoIsCached) cacheInfo(); } /*************************************************************************** * Save information about the result type class in a local cache. * This can speed up processing if the same result type classes are re-used * multiple times. * *

* Note that this method operates regardless of any previous settings made by * calling {@link #enableCaching enableCaching()}. * * @see #enableCaching enableCaching() * * @since 1.19, 2008-09-07 */ public void cacheInfo() { // Ensure that there is a cache synchronized (SqlObjectQuery.class) { if (s_cache == null) s_cache = new HashMap(20); } // Add info about the class type to the cache if (s_cache.get(m_type) == null) { synchronized (s_cache) { Object[] info; info = new Object[2]; info[0] = m_ctor; info[1] = m_methods; s_cache.put(m_type, info); } } } /*************************************************************************** * Add a field expression to the SELECT clause of the SQL query. * *

* A specified SQL field expression (e.g., "employees.employee_id * as eid") is included as part of the SQL query to be * executed by {@link #executeQuery executeQuery()}. It is paired with a * specific member of the ResultType class. The specified member is * either the name of a member variable (e.g., "employeeID") or the * name of a setter method (e.g., "setEmployeeID()"). In either * case, the type of the SQL field must be assignable to the member variable * or be passable as the single argument of the setter method. * *

* The Java types supported are: *

* * @param field * The column name as it appears in the SELECT clause of the SQL * query (e.g., "e.employee_id"). The name can also include an * alias clause (e.g., "e.employee_id id") to be used in the rest of * the SQL query. *

* * @param member * The name of the member variable or setter method of the * ResultType class that the field is to be assigned to when the * query result object is retrieved. If the name ends with "()", it * specifies a setter method to be called to assign the result value; * otherwise it specifies a member variable to be assigned. * The name can be null, in which case the column name will not be assigned * to any member of class ResultType (presumably this specifies a * column or expression that is used in a GROUP BY clause.) * * @throws SQLException * Thrown if var does not name a member variable or method of the * result type class, or if the named member is not of one of the supported * types. * * @since 1.1, 2008-02-14 */ public void addField(String field, String member) throws SQLException { // Consistency check if (m_mappings == null) throw new SQLException("Query has been closed"); if (m_query != null) throw new SQLException("Cannot change query after executeQuery()" + " has been called"); // Find the named member variable or method for the result field if (member != null && member.length() > 0) { if (member.endsWith("()")) { member = member.substring(0, member.length() - 2); addFieldToSetterMap(field, member); } else addFieldToVariableMap(field, member); } else { // Add an unmapped field to the list m_fields.add(field); m_mappings.add(null); } } /*************************************************************************** * Determine the mapping from an SQL SELECT field to a setter member * method of the Java result object class. * * @param field * The column name as it appears in the SELECT clause of the SQL * query (e.g., "e.employee_id". *

* * @param member * The name of the member setter method of the ResultType class that * the field is to be assigned to when the query result object is retrieved. * * @throws SQLException * Thrown if var does not name a member method of the result type * class, or if the named method does not take a single argument of one of * the basic types supported by SQL. * * @since 1.1, 2008-02-26 */ private void addFieldToSetterMap(String field, String member) throws SQLException { try { Method meth; Field fld; int mods; Class type; FieldTypeMap fact; // Find a suitable setter method for the result field meth = (Method) m_methods.get(member); if (meth == null) throw new SQLException( "No unique matching setter method found: " + member + "()"); // Determine the type of the member variable type = meth.getParameterTypes()[0]; fact = FieldTypeMap.getInstance(type); if (fact == null) throw new SQLException( "Setter method parameter is not an assignable type: " + member + "(): " + type.getName()); // Add the mapped field to the list m_fields.add(field); m_mappings.add(fact.create(meth)); } catch (Exception ex) { // Inaccessible or nonexistent member variable throw new SQLException( "Inaccessible or nonexistent class method: " + member + "()"); } } /*************************************************************************** * Determine the mapping from an SQL SELECT field to a member * variable of the Java result object class. * * @param field * The column name as it appears in the SELECT clause of the SQL * query (e.g., "e.employee_id". *

* * @param member * The name of the member variable of the ResultType class that the * field is to be assigned to when the query result object is retrieved. * * @throws SQLException * Thrown if var does not name a member variable of the result type * class, or if the named member is not of one of the basic types supported * by SQL. * * @since 1.1, 2008-02-26 */ private void addFieldToVariableMap(String field, String member) throws SQLException { try { Field var; int mods; FieldTypeMap fact; // Find the class variable or setter method for the variable var = m_type.getDeclaredField(member); // Verify that the member variable is assignable mods = var.getModifiers(); if ((mods & Modifier.FINAL) != 0) throw new SQLException("Class variable is final: " + member); if ((mods & Modifier.STATIC) != 0) throw new SQLException("Class variable is static: " + member); // Determine the type of the member variable fact = FieldTypeMap.getInstance(var.getType()); if (fact == null) throw new SQLException( "Class variable is not an assignable type: " + member + ": " + var.getType().getName()); // Add the mapped field to the list m_fields.add(field); m_mappings.add(fact.create(var)); } catch (Exception ex) { // Inaccessible or nonexistent member variable throw new SQLException( "Inaccessible or nonexistent class variable: " + member); } } /*************************************************************************** * Add a leading clause to the SELECT clause of the SQL query. * This applies to the operands of the SELECT clause immediately * preceding the selected field (column) names. * *

* For reference, a standard SQL query looks like this: *

    *    SELECT [option...] column [alias] [, ...]
    *    FROM table [alias]
    *    [ JOIN table [alias] ON expr ]
    *    [ WHERE expr ]
    *    [ ORDER BY expr [, ...] ]
    *    [ GROUP BY expr [, ...] [ HAVING expr ] ]
* *

* Note that this method should only be called once on a given query object. * * @throws SQLException * Thrown if {@link #executeQuery executeQuery()}, * {@link #getQueryString getQueryString()}, or {@link #close close()} * has been invoked. * * @since 1.1, 2008-02-14 */ public void addSelect(String clause) throws SQLException { // Consistency check if (m_mappings == null) throw new SQLException("Query has been closed"); if (m_query != null) throw new SQLException("Cannot change query after executeQuery()" + " has been called"); // Prepend a clause to the SQL query SELECT clause if (m_select == null) m_select = new StringBuffer(clause.length() + 10); if (m_select.length() > 0) m_select.append(' '); m_select.append(clause); } /*************************************************************************** * Add a trailing clause to the SQL query. * This applies to all clauses starting with the FROM clause. * Note that the clauses must be added in the correct order by the caller in * order to construct a syntactically correct query. * *

* For reference, a standard SQL query looks like this: *

    *    SELECT [clause...] column [[AS] alias] [, ...]
    *    FROM table [alias]
    *    [ JOIN table [alias] ON expr ]
    *    [ WHERE expr ]
    *    [ ORDER BY expr [, ...] ]
    *    [ GROUP BY expr [, ...] [ HAVING expr ] ]
* * @throws SQLException * Thrown if {@link #executeQuery executeQuery()}, * {@link #getQueryString getQueryString()}, or {@link #close close()} * has been invoked. * * @since 1.1, 2008-02-14 */ public void addClause(String clause) throws SQLException { // Consistency check if (m_mappings == null) throw new SQLException("Query has been closed"); if (m_query != null) throw new SQLException("Cannot change query after executeQuery()" + " has been called"); // Append the clause to the SQL query if (m_clauses == null) m_clauses = new StringBuffer(80); if (m_clauses.length() > 0) m_clauses.append(' '); m_clauses.append(clause); } /*************************************************************************** * Retrieve the SQL query string to be executed. * *

* Note that once this method has been called, the query cannot be modified * any further by calling {@link #addSelect addSelect()}, * {@link #addField addField()}, or {@link #addClause addClause()}; doing so * will cause an exception to be thrown. * * @return * The fully constructed SQL query that will be executed against the * database. * * @since 1.7, 2008-02-29 */ public String getQueryString() { // Consistency check if (m_mappings == null) return ""; // Build the complete SQL query if (m_query == null) { StringBuffer query = new StringBuffer(150); query.append("SELECT"); if (m_select != null) { query.append(' '); query.append(m_select); } for (int i = 0; i < m_fields.size(); i++) { if (i > 0) query.append(','); query.append(' '); query.append(m_fields.get(i)); } if (m_clauses != null) { query.append(' '); query.append(m_clauses); } m_query = query.toString(); } return m_query; } /*************************************************************************** * Execute the SQL query. * The results of the query are stored in this object, and can be retrieved * by {@link SqlObjectResultSet#getResult() SqlObjectResultSet.getResult()}. * *

* Note that once this method has been called, the query cannot be modified * any further by calling {@link #addSelect addSelect()}, * {@link #addField addField()}, or {@link #addClause addClause()}; doing so * will cause an exception to be thrown. * *

* Note also that the query object can be reused, calling its * {@link #executeQuery executeQuery()} method multiple times to get multiple * result sets. * * @param stmt * A JDBC statement with which to execute the SQL query. * * @return * The result set of the query if it was successful, otherwise an exception * is thrown. * * @throws SQLException * Thrown if an error occurs while executing the SQL query. * * @since 1.7, 2008-02-29 */ public SqlObjectResultSet executeQuery(Statement stmt) throws SQLException { ResultSet results = null; // Sanity check if (m_mappings == null) throw new SQLException("Query has been closed"); if (stmt == null) throw new NullPointerException("Missing JDBC statement"); if (m_clauses == null) throw new SQLException("Missing SQL query;" + " use addSelect(), addField(), and addClause()"); // Build the complete SQL query if (m_query == null) getQueryString(); // Perform the SQL query try { // Execute the SQL query results = stmt.executeQuery(m_query); // Return an iterator for the query result set return new SqlObjectResultSet(this, m_type, m_ctor, m_mappings, results, stmt); } catch (SQLException ex) { // SQL query failed, clean it up try { if (results != null) results.close(); } catch (SQLException ex2) { } try { stmt.close(); } catch (SQLException ex2) { } // Re-throw the exception throw ex; } } /*************************************************************************** * Shut down this query object. * Subsequent attempts to modify this object or invoke * {@link #executeQuery executeQuery()} result in exceptions. * * @since 1.12, 2008-09-03 */ public void close() { // Clean up if (m_mappings != null) { m_fields = null; m_methods = null; m_mappings = null; m_select = null; m_clauses = null; m_query = null; } } } // End SqlObjectQuery.java