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

com.github.davidmoten.rx.jdbc.QueryUpdate Maven / Gradle / Ivy

There is a newer version: 0.7.19
Show newest version
package com.github.davidmoten.rx.jdbc;

import static com.github.davidmoten.rx.jdbc.Conditions.checkArgument;
import static com.github.davidmoten.rx.jdbc.Conditions.checkNotNull;
import static com.github.davidmoten.rx.jdbc.Queries.bufferedParameters;

import java.sql.ResultSet;
import java.util.List;

import com.github.davidmoten.rx.Transformers;
import com.github.davidmoten.rx.jdbc.NamedParameters.JdbcQuery;
import com.github.davidmoten.rx.jdbc.tuple.Tuple2;
import com.github.davidmoten.rx.jdbc.tuple.Tuple3;
import com.github.davidmoten.rx.jdbc.tuple.Tuple4;
import com.github.davidmoten.rx.jdbc.tuple.Tuple5;
import com.github.davidmoten.rx.jdbc.tuple.Tuple6;
import com.github.davidmoten.rx.jdbc.tuple.Tuple7;
import com.github.davidmoten.rx.jdbc.tuple.TupleN;
import com.github.davidmoten.rx.jdbc.tuple.Tuples;

import rx.Observable;
import rx.Observable.Transformer;
import rx.functions.Func1;
import rx.functions.Func2;

/**
 * Always emits an Observable of size 1 containing the number of
 * affected records.
 * 
 * @param 
 *            type of returned observable (Integer for count, custom for
 *            returning generated keys)
 */
final public class QueryUpdate implements Query {

    private final JdbcQuery jdbcQuery;
    private final Observable parameters;
    private final QueryContext context;
    private final Observable depends;
    // nullable!
    private final ResultSetMapper returnGeneratedKeysFunction;
    private static final Func1, List> toFinalArrayList = new Func1, List>() {
        @Override
        public List call(List list) {
            return new ArrayListFinal(list);
        }
    };

    /**
     * Private constructor.
     * 
     * @param sql
     * @param parameters
     * @param depends
     * @param context
     * @param returnGeneratedKeysFunction
     *            nullable!
     */
    private QueryUpdate(String sql, Observable parameters, Observable depends,
            QueryContext context, ResultSetMapper returnGeneratedKeysFunction) {
        checkNotNull(sql);
        checkNotNull(parameters);
        checkNotNull(depends);
        checkNotNull(context);
        checkArgument(context.batchSize() <= 1 || returnGeneratedKeysFunction == null,
                "Cannot return generated keys from a batched update");
        this.jdbcQuery = NamedParameters.parse(sql);
        this.parameters = parameters;
        this.depends = depends;
        this.context = context;
        this.returnGeneratedKeysFunction = returnGeneratedKeysFunction;
    }

    @Override
    public String sql() {
        return jdbcQuery.sql();
    }

    @Override
    public Observable parameters() {
        return parameters;
    }

    @Override
    public QueryContext context() {
        return context;
    }

    @Override
    public String toString() {
        return "QueryUpdate [sql=" + sql() + "]";
    }

    @Override
    public Observable depends() {
        return depends;
    }

    @Override
    public List names() {
        return jdbcQuery.names();
    }

    /**
     * Returns the results of an update query. Should be an {@link Observable}
     * of size 1 containing the number of records affected by the update (or
     * insert) statement.
     * 
     * @param query
     * @return
     */
    @SuppressWarnings("unchecked")
    public Observable count() {
        return (Observable) QueryUpdate.get(this);
    }

    public ResultSetMapper returnGeneratedKeysFunction() {
        return returnGeneratedKeysFunction;
    }

    boolean returnGeneratedKeys() {
        return returnGeneratedKeysFunction != null;
    }

    static  Observable get(QueryUpdate queryUpdate) {
        if (queryUpdate.context().batchSize() > 1) {
            return bufferedParameters(queryUpdate) //
                    // mark the last parameter list as such
                    .compose(Transformers.mapLast(toFinalArrayList))//
                    // execute query for each set of parameters
                    .concatMap(queryUpdate.executeOnce());
        } else {
            return bufferedParameters(queryUpdate) //
                    // execute query for each set of parameters
                    .concatMap(queryUpdate.executeOnce());
        }
    }

    /**
     * Returns a {@link Func1} that itself returns the results of pushing
     * parameters through an update query.
     * 
     * @param query
     * @return
     */
    private Func1, Observable> executeOnce() {
        return new Func1, Observable>() {
            @Override
            public Observable call(final List params) {
                if (jdbcQuery.sql().equals(QueryUpdateOnSubscribe.BEGIN_TRANSACTION)) {
                    context.beginTransactionSubscribe();
                }
                Observable result = createUpdate(params).subscribeOn(context.scheduler());
                if (jdbcQuery.sql().equals(QueryUpdateOnSubscribe.COMMIT)
                        || jdbcQuery.sql().equals(QueryUpdateOnSubscribe.ROLLBACK))
                    context.endTransactionSubscribe();
                return result;
            }
        };
    }

    /**
     * Returns the results of an update query. Should return an
     * {@link Observable} of size one containing the rows affected count.
     * 
     * @param query
     * @param parameters
     * @return
     */
    private Observable createUpdate(final List parameters) {
        return QueryUpdateOnSubscribe.create(this, parameters);
    }

    /**
     * Builds a {@link QueryUpdate}.
     */
    final public static class Builder {

        private static final int DEFAULT_BATCH_SIZE = 1;
        /**
         * Standard query builder.
         */
        private final QueryBuilder builder;
        private int batchSize = DEFAULT_BATCH_SIZE;

        /**
         * Constructor.
         * 
         * @param sql
         * @param db
         */
        public Builder(String sql, Database db) {
            this.builder = new QueryBuilder(sql, db);
        }

        /**
         * Appends the given parameters to the parameter list for the query. If
         * there are more parameters than required for one execution of the
         * query then more than one execution of the query will occur.
         * 
         * @param parameters
         * @return this
         */
        public  Builder parameters(Observable parameters) {
            builder.parameters(parameters);
            return this;
        }

        /**
         * Appends the given parameter values to the parameter list for the
         * query. If there are more parameters than required for one execution
         * of the query then more than one execution of the query will occur.
         * 
         * @param objects
         * @return this
         */
        public Builder parameters(Object... objects) {
            builder.parameters(objects);
            return this;
        }

        /**
         * Appends a parameter to the parameter list for the query. If there are
         * more parameters than required for one execution of the query then
         * more than one execution of the query will occur.
         * 
         * @param value
         * @return this
         */
        public Builder parameter(Object value) {
            builder.parameter(value);
            return this;
        }

        /**
         * Sets a named parameter. If name is null throws a
         * {@link NullPointerException}. If value is instance of Observable then
         * throws an {@link IllegalArgumentException}.
         * 
         * @param name
         *            the parameter name. Cannot be null.
         * @param value
         *            the parameter value
         */
        public Builder parameter(String name, Object value) {
            builder.parameter(name, value);
            return this;
        }

        /**
         * Appends a parameter to the parameter list for the query for a CLOB
         * parameter and handles null appropriately. If there are more
         * parameters than required for one execution of the query then more
         * than one execution of the query will occur.
         * 
         * @param value
         *            the string to insert in the CLOB column
         * @return this
         */
        public Builder parameterClob(String value) {
            builder.parameter(Database.toSentinelIfNull(value));
            return this;
        }

        /**
         * Appends a parameter to the parameter list for the query for a CLOB
         * parameter and handles null appropriately. If there are more
         * parameters than required for one execution of the query then more
         * than one execution of the query will occur.
         * 
         * @param value
         * @return this
         */
        public Builder parameterBlob(byte[] bytes) {
            builder.parameter(Database.toSentinelIfNull(bytes));
            return this;
        }

        /**
         * Appends a dependency to the dependencies that have to complete their
         * emitting before the query is executed.
         * 
         * @param dependency
         * @return this
         */
        public Builder dependsOn(Observable dependency) {
            builder.dependsOn(dependency);
            return this;
        }

        /**
         * Appends a dependency on the result of the last transaction (
         * true for commit or false for rollback) to
         * the dependencies that have to complete their emitting before the
         * query is executed.
         * 
         * @return this
         */
        public Builder dependsOnLastTransaction() {
            builder.dependsOnLastTransaction();
            return this;
        }

        /**
         * Returns a builder used to specify how to process the generated keys
         * {@link ResultSet}. Not all jdbc drivers support this functionality
         * and some have limitations in their support (h2 for instance only
         * returns the last generated key when multiple inserts happen in the
         * one statement).
         * 
         * @return a builder used to specify how to process the generated keys
         *         ResultSet
         */
        public ReturnGeneratedKeysBuilder returnGeneratedKeys() {
            Conditions.checkArgument(batchSize == 1,
                    "Cannot return generated keys if batchSize > 1");
            return new ReturnGeneratedKeysBuilder(builder);
        }

        /**
         * Returns an {@link Observable} with the count of rows affected by the
         * update statement.
         * 
         * @return Observable of counts of rows affected.
         */
        public Observable count() {
            QueryContext ctxt;
            if (batchSize > 1) {
                ctxt = builder.context().batched(batchSize);
            } else {
                ctxt = builder.context();
            }
            return new QueryUpdate(builder.sql(), builder.parameters(), builder.depends(),
                    ctxt, null).count();
        }

        /**
         * Executes the update query immediately, blocking till completion and
         * returns total of counts of records affected.
         * 
         * @return total of counts of records affected by update queries
         */
        public int execute() {
            return count().reduce(0, TotalHolder.TOTAL).toBlocking().single();
        }

        private static final class TotalHolder {
            static final Func2 TOTAL = new Func2() {

                @Override
                public Integer call(Integer a, Integer b) {
                    return a + b;
                }
            };
        }

        /**
         * Returns an {@link Transformer} to allow the query to be pushed
         * parameters via the {@link Observable#compose(Transformer)} method.
         * 
         * @return Transformer that acts on parameters
         */
        public Transformer parameterTransformer() {
            return new QueryUpdateTransformer(this, OperatorType.PARAMETER);
        }

        /**
         * Returns an {@link Transformer} to allow the query to be pushed
         * dependencies via the {@link Observable#compose(Transformer)} method.
         * 
         * @return Transformer that acts on dependencies
         */
        public Transformer dependsOnTransformer() {
            return new QueryUpdateTransformer(this, OperatorType.DEPENDENCY);
        }

        /**
         * Returns an {@link Transformer} to allow the query to be run once per
         * parameter list in the source.
         * 
         * @return transformer
         */
        public Transformer, Observable> parameterListTransformer() {
            return new QueryUpdateTransformerFromObservable(this);
        }

        /**
         * Clears the parameter inputs for the query.
         * 
         * @return the current builder
         */
        public Builder clearParameters() {
            builder.clearParameters();
            return this;
        }

        public Builder batchSize(int batchSize) {
            this.batchSize = batchSize;
            return this;
        }
    }

    public static class ReturnGeneratedKeysBuilder {

        private final QueryBuilder builder;

        public ReturnGeneratedKeysBuilder(QueryBuilder builder) {
            this.builder = builder;
        }

        /**
         * Transforms the results using the given function.
         *
         * @param function
         * @return
         */
        public  Observable get(ResultSetMapper function) {
            return QueryUpdate.get(new QueryUpdate(builder.sql(), builder.parameters(),
                    builder.depends(), builder.context(), function));
        }

        /**
         * 

* Transforms each row of the {@link ResultSet} into an instance of * T using automapping of the ResultSet columns into * corresponding constructor parameters that are assignable. Beyond * normal assignable criteria (for example Integer 123 is assignable to * a Double) other conversions exist to facilitate the automapping: *

*

* They are: *

    *
  • java.sql.Blob ➟ byte[]
  • *
  • java.sql.Blob ➟ java.io.InputStream
  • *
  • java.sql.Clob ➟ String
  • *
  • java.sql.Clob ➟ java.io.Reader
  • *
  • java.sql.Date ➟ java.util.Date
  • *
  • java.sql.Date ➟ Long
  • *
  • java.sql.Timestamp ➟ java.util.Date
  • *
  • java.sql.Timestamp ➟ Long
  • *
  • java.sql.Time ➟ java.util.Date
  • *
  • java.sql.Time ➟ Long
  • *
  • java.math.BigInteger ➟ * Short,Integer,Long,Float,Double,BigDecimal
  • *
  • java.math.BigDecimal ➟ * Short,Integer,Long,Float,Double,BigInteger
  • *

    * * @param cls * @return */ public Observable autoMap(Class cls) { Util.setSqlFromQueryAnnotation(cls, builder); return get(Util.autoMap(cls)); } /** * Automaps the first column of the ResultSet into the target class * cls. * * @param cls * @return */ public Observable getAs(Class cls) { return get(Tuples.single(cls)); } /** * Automaps all the columns of the {@link ResultSet} into the target * class cls. See {@link #autoMap(Class) autoMap()}. * * @param cls * @return */ public Observable> getTupleN(Class cls) { return get(Tuples.tupleN(cls)); } /** * Automaps all the columns of the {@link ResultSet} into {@link Object} * . See {@link #autoMap(Class) autoMap()}. * * @param cls * @return */ public Observable> getTupleN() { return get(Tuples.tupleN(Object.class)); } /** * Automaps the columns of the {@link ResultSet} into the specified * classes. See {@link #autoMap(Class) autoMap()}. * * @param cls1 * @param cls2 * @return */ public Observable> getAs(Class cls1, Class cls2) { return get(Tuples.tuple(cls1, cls2)); } /** * Automaps the columns of the {@link ResultSet} into the specified * classes. See {@link #autoMap(Class) autoMap()}. * * @param cls1 * @param cls2 * @param cls3 * @return */ public Observable> getAs(Class cls1, Class cls2, Class cls3) { return get(Tuples.tuple(cls1, cls2, cls3)); } /** * Automaps the columns of the {@link ResultSet} into the specified * classes. See {@link #autoMap(Class) autoMap()}. * * @param cls1 * @param cls2 * @param cls3 * @param cls4 * @return */ public Observable> getAs(Class cls1, Class cls2, Class cls3, Class cls4) { return get(Tuples.tuple(cls1, cls2, cls3, cls4)); } /** * Automaps the columns of the {@link ResultSet} into the specified * classes. See {@link #autoMap(Class) autoMap()}. * * @param cls1 * @param cls2 * @param cls3 * @param cls4 * @param cls5 * @return */ public Observable> getAs(Class cls1, Class cls2, Class cls3, Class cls4, Class cls5) { return get(Tuples.tuple(cls1, cls2, cls3, cls4, cls5)); } /** * Automaps the columns of the {@link ResultSet} into the specified * classes. See {@link #autoMap(Class) autoMap()}. * * @param cls1 * @param cls2 * @param cls3 * @param cls4 * @param cls5 * @param cls6 * @return */ public Observable> getAs( Class cls1, Class cls2, Class cls3, Class cls4, Class cls5, Class cls6) { return get(Tuples.tuple(cls1, cls2, cls3, cls4, cls5, cls6)); } /** * Automaps the columns of the {@link ResultSet} into the specified * classes. See {@link #autoMap(Class) autoMap()}. * * @param cls1 * @param cls2 * @param cls3 * @param cls4 * @param cls5 * @param cls6 * @param cls7 * @return */ public Observable> getAs( Class cls1, Class cls2, Class cls3, Class cls4, Class cls5, Class cls6, Class cls7) { return get(Tuples.tuple(cls1, cls2, cls3, cls4, cls5, cls6, cls7)); } public Observable count() { return get(Util.toOne()).count(); } } }