All Downloads are FREE. Search and download functionalities are using the official Maven repository.

groovy.sql.DataSet Maven / Gradle / Ivy

/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package groovy.sql;

import groovy.lang.Closure;
import groovy.lang.GroovyRuntimeException;
import groovy.transform.stc.ClosureParams;
import groovy.transform.stc.SimpleType;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.CodeVisitorSupport;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.stmt.Statement;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * An enhancement of Groovy's Sql class providing support for accessing
 * and querying databases using POGO fields and operators rather than
 * JDBC-level API calls and RDBMS column names. So, instead of a query like:
 * 
 * {@code
 * def db = // an instance of groovy.sql.Sql
 * def sql = '''select * from Person
 *     where (purchaseCount > ? and birthMonth = ?)
 *     and (lastName < ? or lastName > ?)
 *     and age < ? and age > ? and firstName != ?
 *     order by firstName DESC, age'''
 * def params = [10, "January", "Zulu", "Alpha", 99, 5, "Bert"]
 * def sortedPeopleOfInterest = db.rows(sql, params)
 * }
 * 
* You can write code like this: *
 * {@code
 * def person = new DataSet(db, 'Person') // or db.dataSet('Person'), or db.dataSet(Person)
 * def janFrequentBuyers = person.findAll { it.purchaseCount > 10 && it.lastName == "January" }
 * def sortedPeopleOfInterest = janFrequentBuyers.
 *     findAll{ it.lastName < 'Zulu' || it.lastName > 'Alpha' }.
 *     findAll{ it.age < 99 }.
 *     findAll{ it.age > 5 }.
 *     sort{ it.firstName }.
 *     reverse().
 *     findAll{ it.firstName != 'Bert' }.
 *     sort{ it.age }
 * }
 * 
* Currently, the Groovy source code for any accessed POGO must be on the * classpath at runtime. Also, at the moment, the expressions (or nested expressions) can only contain * references to fields of the POGO or literals (i.e. constant Strings or numbers). This limitation * may be removed in a future version of Groovy. */ public class DataSet extends Sql { private static final int[] EMPTY_INT_ARRAY = new int[0]; private Closure where; private Closure sort; private boolean reversed = false; private DataSet parent; private final String table; private SqlWhereVisitor visitor; private SqlOrderByVisitor sortVisitor; private String sql; private List params; private List batchData; private Set batchKeys; private final Sql delegate; private boolean withinDataSetBatch = false; public DataSet(Sql sql, Class type) { super(sql); delegate = sql; String table = type.getName(); int idx = table.lastIndexOf('.'); if (idx > 0) { table = table.substring(idx + 1); } this.table = table.toLowerCase(); } public DataSet(Sql sql, String table) { super(sql); delegate = sql; this.table = table; } private DataSet(DataSet parent, Closure where) { super(parent); this.delegate = parent.delegate; this.table = parent.table; this.parent = parent; this.where = where; } private DataSet(DataSet parent, Closure where, Closure sort) { super(parent); this.delegate = parent.delegate; this.table = parent.table; this.parent = parent; this.where = where; this.sort = sort; } private DataSet(DataSet parent) { super(parent); this.delegate = parent.delegate; this.table = parent.table; this.parent = parent; this.reversed = true; } @Override protected Connection createConnection() throws SQLException { return delegate.createConnection(); } @Override protected void closeResources(Connection connection, java.sql.Statement statement, ResultSet results) { delegate.closeResources(connection, statement, results); } @Override protected void closeResources(Connection connection, java.sql.Statement statement) { delegate.closeResources(connection, statement); } @Override public void cacheConnection(Closure closure) throws SQLException { delegate.cacheConnection(closure); } @Override public void withTransaction(Closure closure) throws SQLException { delegate.withTransaction(closure); } @Override public void commit() throws SQLException { delegate.commit(); } @Override public void rollback() throws SQLException { delegate.rollback(); } /** * Performs the closure (containing batch operations) within a batch. * Uses a batch size of zero, i.e. no automatic partitioning of batches. * * @param closure the closure containing batch and optionally other statements * @return an array of update counts containing one element for each * command in the batch. The elements of the array are ordered according * to the order in which commands were added to the batch. * @throws SQLException if a database access error occurs, * or this method is called on a closed Statement, or the * driver does not support batch statements. Throws {@link java.sql.BatchUpdateException} * (a subclass of SQLException) if one of the commands sent to the * database fails to execute properly or attempts to return a result set. */ @Override public int[] withBatch(Closure closure) throws SQLException { return withBatch(0, closure); } /** * Performs the closure (containing batch operations) within a batch. * For example: *
     * dataSet.withBatch(3) {
     *     add(anint: 1, astring: "Groovy")
     *     add(anint: 2, astring: "rocks")
     *     add(anint: 3, astring: "the")
     *     add(anint: 4, astring: "casbah")
     * }
     * 
* * @param batchSize partition the batch into batchSize pieces, i.e. after batchSize * addBatch() invocations, call executeBatch() automatically; * 0 means manual calls to executeBatch are required * @param closure the closure containing batch and optionally other statements * @return an array of update counts containing one element for each * command in the batch. The elements of the array are ordered according * to the order in which commands were added to the batch. * @throws SQLException if a database access error occurs, or the driver does not support batch statements. * Throws {@link java.sql.BatchUpdateException} (a subclass of SQLException) * if one of the commands sent to the database fails to execute properly. */ @Override public int[] withBatch(int batchSize, Closure closure) throws SQLException { batchData = new ArrayList(); withinDataSetBatch = true; closure.call(this); withinDataSetBatch = false; if (batchData.isEmpty()) { return EMPTY_INT_ARRAY; } Closure transformedClosure = new Closure(null) { public void doCall(BatchingPreparedStatementWrapper stmt) throws SQLException { for (Object next : batchData) { stmt.addBatch(new Object[]{next}); } } }; return super.withBatch(batchSize, buildMapQuery(), transformedClosure); } /** * Adds the provided map of key-value pairs as a new row in the table represented by this DataSet. * * @param map the key (column-name), value pairs to add as a new row * @throws SQLException if a database error occurs */ public void add(Map map) throws SQLException { if (withinDataSetBatch) { if (batchData.isEmpty()) { batchKeys = map.keySet(); } else { if (!map.keySet().equals(batchKeys)) { throw new IllegalArgumentException("Inconsistent keys found for batch add!"); } } batchData.add(map); return; } int answer = executeUpdate(buildListQuery(map), new ArrayList(map.values())); if (answer != 1) { LOG.warning("Should have updated 1 row not " + answer + " when trying to add: " + map); } } private String buildListQuery(Map map) { StringBuilder buffer = new StringBuilder("insert into "); buffer.append(table); buffer.append(" ("); StringBuilder paramBuffer = new StringBuilder(); boolean first = true; for (String column : map.keySet()) { if (first) { first = false; paramBuffer.append("?"); } else { buffer.append(", "); paramBuffer.append(", ?"); } buffer.append(column); } buffer.append(") values ("); buffer.append(paramBuffer.toString()); buffer.append(")"); return buffer.toString(); } private String buildMapQuery() { StringBuilder buffer = new StringBuilder("insert into "); buffer.append(table); buffer.append(" ("); StringBuilder paramBuffer = new StringBuilder(); boolean first = true; for (String column : batchKeys) { if (first) { first = false; paramBuffer.append(":"); } else { buffer.append(", "); paramBuffer.append(", :"); } paramBuffer.append(column); buffer.append(column); } buffer.append(") values ("); buffer.append(paramBuffer.toString()); buffer.append(")"); return buffer.toString(); } /** * Return a lazy-implemented filtered view of this DataSet. * * @param where the filtering Closure * @return the view DataSet */ public DataSet findAll(Closure where) { return new DataSet(this, where); } /** * Return a lazy-implemented re-ordered view of this DataSet. * * @param sort the ordering Closure * @return the view DataSet */ public DataSet sort(Closure sort) { return new DataSet(this, null, sort); } /** * Return a lazy-implemented reverse-ordered view of this DataSet. * * @return the view DataSet */ public DataSet reverse() { if (sort == null) { throw new GroovyRuntimeException("reverse() only allowed immediately after a sort()"); } return new DataSet(this); } /** * Calls the provided closure for each of the rows of the table represented by this DataSet. * * @param closure called for each row with a GroovyResultSet * @throws SQLException if a database access error occurs * @see groovy.sql.Sql#eachRow(String, java.util.List, groovy.lang.Closure) */ public void each(@ClosureParams(value=SimpleType.class, options="groovy.sql.GroovyResultSet") Closure closure) throws SQLException { eachRow(getSql(), getParameters(), closure); } /** * Calls the provided closure for a "page" of rows from the table represented by this DataSet. * A page is defined as starting at a 1-based offset, and containing a maximum number of rows. * * @param offset the 1-based offset for the first row to be processed * @param maxRows the maximum number of rows to be processed * @param closure called for each row with a GroovyResultSet * @throws SQLException if a database access error occurs * @see groovy.sql.Sql#eachRow(String, java.util.List, int, int, groovy.lang.Closure) */ public void each(int offset, int maxRows, @ClosureParams(value=SimpleType.class, options="groovy.sql.GroovyResultSet") Closure closure) throws SQLException { eachRow(getSql(), getParameters(), offset, maxRows, closure); } private String getSqlWhere() { String whereClaus = ""; String parentClaus = ""; if (parent != null) { parentClaus = parent.getSqlWhere(); } if (where != null) { whereClaus += getSqlWhereVisitor().getWhere(); } if (parentClaus.length() == 0) return whereClaus; if (whereClaus.length() == 0) return parentClaus; return parentClaus + " and " + whereClaus; } private String getSqlOrderBy() { String sortByClaus = ""; String parentClaus = ""; if (parent != null) { parentClaus = parent.getSqlOrderBy(); } if (reversed) { if (parentClaus.length() > 0) parentClaus += " DESC"; } if (sort != null) { sortByClaus += getSqlOrderByVisitor().getOrderBy(); } if (parentClaus.length() == 0) return sortByClaus; if (sortByClaus.length() == 0) return parentClaus; return parentClaus + ", " + sortByClaus; } public String getSql() { if (sql == null) { sql = "select * from " + table; String whereClaus = getSqlWhere(); if (whereClaus.length() > 0) { sql += " where " + whereClaus; } String orderByClaus = getSqlOrderBy(); if (orderByClaus.length() > 0) { sql += " order by " + orderByClaus; } } return sql; } public List getParameters() { if (params == null) { params = new ArrayList(); if (parent != null) { params.addAll(parent.getParameters()); } params.addAll(getSqlWhereVisitor().getParameters()); } return params; } protected SqlWhereVisitor getSqlWhereVisitor() { if (visitor == null) { visitor = new SqlWhereVisitor(); visit(where, visitor); } return visitor; } protected SqlOrderByVisitor getSqlOrderByVisitor() { if (sortVisitor == null) { sortVisitor = new SqlOrderByVisitor(); visit(sort, sortVisitor); } return sortVisitor; } private static void visit(Closure closure, CodeVisitorSupport visitor) { if (closure != null) { ClassNode classNode = closure.getMetaClass().getClassNode(); if (classNode == null) { throw new GroovyRuntimeException( "DataSet unable to evaluate expression. AST not available for closure: " + closure.getMetaClass().getTheClass().getName() + ". Is the source code on the classpath?"); } List methods = classNode.getDeclaredMethods("doCall"); if (!methods.isEmpty()) { MethodNode method = (MethodNode) methods.get(0); if (method != null) { Statement statement = method.getCode(); if (statement != null) { statement.visit(visitor); } } } } } /* * create a subset of the original dataset */ public DataSet createView(Closure criteria) { return new DataSet(this, criteria); } /** * Returns a List of all of the rows from the table a DataSet * represents. * * @return Returns a list of GroovyRowResult objects from the dataset * @throws SQLException if a database error occurs */ public List rows() throws SQLException { return rows(getSql(), getParameters()); } /** * Returns a "page" of the rows from the table a DataSet represents. A page * is defined as starting at a 1-based offset, and containing a maximum number * of rows. * * @param offset the 1-based offset for the first row to be processed * @param maxRows the maximum number of rows to be processed * @return a list of GroovyRowResult objects from the dataset * @throws SQLException if a database error occurs */ public List rows(int offset, int maxRows) throws SQLException { return rows(getSql(), getParameters(), offset, maxRows); } /** * Returns the first row from a DataSet's underlying table * * @return Returns the first GroovyRowResult object from the dataset * @throws SQLException if a database error occurs */ public Object firstRow() throws SQLException { List rows = rows(); if (rows.isEmpty()) return null; return (rows.get(0)); } @Override public void close() { delegate.close(); super.close(); } }