com.github.davidmoten.rx.jdbc.Database Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of rxjava-jdbc Show documentation
Show all versions of rxjava-jdbc Show documentation
rx-java Observables for jdbc
package com.github.davidmoten.rx.jdbc;
import static com.github.davidmoten.rx.RxUtil.constant;
import static com.github.davidmoten.rx.RxUtil.greaterThanZero;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Types;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rx.Observable;
import rx.Observable.Operator;
import rx.Scheduler;
import rx.functions.Func0;
import rx.functions.Func1;
import rx.functions.Func2;
import rx.observables.StringObservable;
import rx.schedulers.Schedulers;
import com.github.davidmoten.rx.RxUtil;
import com.github.davidmoten.rx.RxUtil.CountingAction;
/**
* Main entry point for manipulations of a database using rx-java-jdbc style
* queries.
*/
final public class Database {
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(Database.class);
/**
* Provides access for queries to a limited subset of {@link Database}
* methods.
*/
private final QueryContext context;
/**
* ThreadLocal storage of the current {@link Scheduler} factory to use with
* queries.
*/
private final ThreadLocal> currentSchedulerFactory = new ThreadLocal>();
/**
* ThreadLocal storage of the current {@link ConnectionProvider} to use with
* queries.
*/
private final ThreadLocal currentConnectionProvider = new ThreadLocal();
private final ThreadLocal isTransactionOpen = new ThreadLocal();
/**
* Records the result of the last finished transaction (committed =
* true
or rolled back = false
).
*/
private final ThreadLocal> lastTransactionResult = new ThreadLocal>();
/**
* Connection provider.
*/
private final ConnectionProvider cp;
/**
* Schedules non transactional queries.
*/
private final Func0 nonTransactionalSchedulerFactory;
/**
* Constructor.
*
* @param cp
* provides connections
* @param nonTransactionalSchedulerFactory
* schedules non transactional queries
*/
public Database(final ConnectionProvider cp, Func0 nonTransactionalSchedulerFactory) {
Conditions.checkNotNull(cp);
this.cp = cp;
currentConnectionProvider.set(cp);
if (nonTransactionalSchedulerFactory != null)
this.nonTransactionalSchedulerFactory = nonTransactionalSchedulerFactory;
else
this.nonTransactionalSchedulerFactory = CURRENT_THREAD_SCHEDULER_FACTORY;
this.context = new QueryContext(this);
}
/**
* Returns the {@link ConnectionProvider}.
*
* @return
*/
public ConnectionProvider getConnectionProvider() {
return cp;
}
/**
* Schedules on {@link Schedulers#io()}.
*/
private final Func0 IO_SCHEDULER_FACTORY = new Func0() {
@Override
public Scheduler call() {
return Schedulers.io();
}
};
/**
* Schedules using {@link Schedulers}.trampoline().
*/
private static final Func0 CURRENT_THREAD_SCHEDULER_FACTORY = new Func0() {
@Override
public Scheduler call() {
return Schedulers.trampoline();
}
};
/**
* Constructor. Thread pool size defaults to
* {@link Runtime#getRuntime()}.availableProcessors()+1
. This
* may be too conservative if the database is on another server. If that is
* the case then you may want to use a thread pool size equal to the
* available processors + 1 on the database server.
*
* @param cp
* provides connections
*/
public Database(ConnectionProvider cp) {
this(cp, null);
}
/**
* Constructor. Uses a {@link ConnectionProviderFromUrl} based on the given
* url.
*
* @param url
* jdbc url
* @param username
* username for connection
* @param password
* password for connection
*/
public Database(String url, String username, String password) {
this(new ConnectionProviderFromUrl(url, username, password));
}
/**
* Constructor. Uses the single connection provided and current thread
* scheduler (trampoline) to run all queries. The connection will not be
* closed in reality though the log may indicate it as having received a
* close call.
*
* @param con
* the connection
*/
public Database(Connection con) {
this(new ConnectionProviderNonClosing(con), CURRENT_THREAD_SCHEDULER_FACTORY);
}
/**
* Returns a {@link Database} based on a jdbc connection string.
*
* @param url
* jdbc connection url
* @return
*/
public static Database from(String url) {
return new Database(url, null, null);
}
/**
* Returns a {@link Database} based on a jdbc connection string.
*
* @param url
* jdbc url
* @param username
* username for connection
* @param password
* password for connection
* @return the database object
*/
public static Database from(String url, String username, String password) {
return new Database(url, username, password);
}
/**
* Returns a {@link Database} based on connections obtained from a
* javax.activation.DataSource based on looking up the current
* javax.naming.Context.
*
* @param jndiResource
* @return
*/
public static Database fromContext(String jndiResource) {
return new Database(new ConnectionProviderFromContext(jndiResource));
}
/**
* Returns a {@link Database} that obtains {@link Connection}s on demand
* from the given {@link ConnectionProvider}. When {@link Database#close()}
* is called, {@link ConnectionProvider#close()} is called.
*
* @param cp
* @return
*/
public static Database from(ConnectionProvider cp) {
return new Database(cp);
}
/**
* Factory method. Uses the single connection provided and current thread
* scheduler (trampoline) to run all queries. The connection will not be
* closed in reality though the log may indicate it as having received a
* close call.
*
* @param con
* the connection
*/
public static Database from(Connection con) {
return new Database(con);
}
/**
* Returns a new {@link Builder}.
*
* @return
*/
public static Builder builder() {
return new Builder();
}
/**
* Builds a {@link Database}.
*/
public final static class Builder {
private ConnectionProvider cp;
private Func0 nonTransactionalSchedulerFactory = null;
private Pool pool = null;
private String url;
private String username;
private String password;
private static class Pool {
int minSize;
int maxSize;
Pool(int minSize, int maxSize) {
super();
this.minSize = minSize;
this.maxSize = maxSize;
}
}
/**
* Constructor.
*/
private Builder() {
}
/**
* Sets the connection provider.
*
* @param cp
* @return
*/
public Builder connectionProvider(ConnectionProvider cp) {
this.cp = cp;
return this;
}
/**
* Sets the jdbc url.
*
* @param url
* @return
*/
public Builder url(String url) {
this.url = url;
return this;
}
public Builder username(String username) {
this.username = username;
return this;
}
public Builder password(String password) {
this.password = password;
return this;
}
/**
* Sets the {@link ConnectionProvider} to use a connection pool with the
* given jdbc url and pool size.
*
* @param url
* @param minPoolSize
* @param maxPoolSize
* @return
*/
public Builder pool(int minPoolSize, int maxPoolSize) {
pool = new Pool(minPoolSize, maxPoolSize);
return this;
}
/**
* Sets the {@link ConnectionProvider} to use a connection pool with the
* given jdbc url and min pool size of 0, max pool size of 10.
*
* @param url
* @return
*/
public Builder pooled(String url) {
this.cp = new ConnectionProviderPooled(url, 0, 10);
return this;
}
/**
* Sets the non transactional scheduler.
*
* @param factory
* @return
*/
public Builder nonTransactionalScheduler(Func0 factory) {
nonTransactionalSchedulerFactory = factory;
return this;
}
/**
* Requests that the non transactional queries are run using
* {@link Schedulers#trampoline()}.
*
* @return
*/
public Builder nonTransactionalSchedulerOnCurrentThread() {
nonTransactionalSchedulerFactory = CURRENT_THREAD_SCHEDULER_FACTORY;
return this;
}
/**
* Returns a {@link Database}.
*
* @return
*/
public Database build() {
if (url != null && pool != null)
cp = new ConnectionProviderPooled(url, username, password, pool.minSize,
pool.maxSize);
else if (url != null)
cp = new ConnectionProviderFromUrl(url, username, password);
return new Database(cp, nonTransactionalSchedulerFactory);
}
}
/**
* Returns the thread local current query context (will not return null).
* Will return overriden context (for example using Database returned from
* {@link Database#beginTransaction()} if set.
*
* @return
*/
public QueryContext queryContext() {
return context;
}
/**
* Returns a {@link QuerySelect.Builder} builder based on the given select
* statement sql.
*
* @param sql
* a select statement.
* @return select query builder
*/
public QuerySelect.Builder select(String sql) {
return new QuerySelect.Builder(sql, this);
}
/**
* Returns a {@link QueryUpdate.Builder} builder based on the given
* update/insert/delete/DDL statement sql.
*
* @param sql
* an update/insert/delete/DDL statement.
* @return update/insert query builder
*/
public QueryUpdate.Builder update(String sql) {
return new QueryUpdate.Builder(sql, this);
}
/**
* Starts a transaction. Until commit() or rollback() is called on the
* source this will set the query context for all created queries to be a
* single threaded executor with one (new) connection.
*
* @param dependency
* @return
*/
public Observable beginTransaction(Observable> dependency) {
return update("begin").dependsOn(dependency).count().map(constant(true));
}
/**
* Starts a transaction. Until commit() or rollback() is called on the
* source this will set the query context for all created queries to be a
* single threaded executor with one (new) connection.
*
* @return
*/
public Observable beginTransaction() {
return beginTransaction(Observable.empty());
}
/**
* Returns true if and only if integer is non-zero.
*/
private static final Func1 IS_NON_ZERO = new Func1() {
@Override
public Boolean call(Integer i) {
return i != 0;
}
};
/**
* Commits a transaction and resets the current query context so that
* further queries will use the asynchronous version by default. All
* Observable dependencies must be complete before commit is called.
*
* @param depends
* depdencies that must complete before commit occurs.
* @return
*/
public Observable commit(Observable>... depends) {
return commitOrRollback(true, depends);
}
/**
* Waits for the source to complete before returning the result of
* db.commit();
*
* @return commit operator
*/
public Operator commitOperator() {
return commitOrRollbackOperator(true);
}
/**
* Waits for the source to complete before returning the result of
* db.rollback();
*
* @return rollback operator
*/
public Operator rollbackOperator() {
return commitOrRollbackOperator(false);
}
private Operator commitOrRollbackOperator(final boolean commit) {
final QueryUpdate.Builder updateBuilder = createCommitOrRollbackQuery(commit);
return RxUtil.toOperator(new Func1, Observable>() {
@Override
public Observable call(Observable source) {
return updateBuilder.dependsOn(source).count().map(IS_NON_ZERO);
}
});
}
/**
* Commits or rolls back a transaction depending on the commit
* parameter and resets the current query context so that further queries
* will use the asynchronous version by default. All Observable dependencies
* must be complete before commit/rollback is called.
*
* @param commit
* @param depends
* @return
*/
private Observable commitOrRollback(boolean commit, Observable>... depends) {
QueryUpdate.Builder u = createCommitOrRollbackQuery(commit);
for (Observable> dep : depends)
u = u.dependsOn(dep);
Observable result = u.count().map(IS_NON_ZERO);
lastTransactionResult.set(result);
return result;
}
private QueryUpdate.Builder createCommitOrRollbackQuery(boolean commit) {
String action;
if (commit)
action = "commit";
else
action = "rollback";
QueryUpdate.Builder u = update(action);
return u;
}
/**
* Rolls back a transaction and resets the current query context so that
* further queries will use the asynchronous version by default. All
* Observable dependencies must be complete before rollback is called.
*
* @param depends
* depdencies that must complete before commit occurs.
* @return
*
**/
public Observable rollback(Observable>... depends) {
return commitOrRollback(false, depends);
}
/**
* Returns observable that emits true when last transaction committed or
* false when last transaction is rolled back.
*
* @return
*/
public Observable lastTransactionResult() {
Observable o = lastTransactionResult.get();
if (o == null)
return Observable.empty();
else
return o;
}
/**
* Close the database in particular closes the {@link ConnectionProvider}
* for the database. For a {@link ConnectionProviderPooled} this will be a
* required call for cleanup.
*
* @return
*/
public Database close() {
log.debug("closing connection provider");
cp.close();
log.debug("closed connection provider");
return this;
}
/**
* Returns the current thread local {@link Scheduler}.
*
* @return
*/
Scheduler currentScheduler() {
if (currentSchedulerFactory.get() == null)
return nonTransactionalSchedulerFactory.call();
else
return currentSchedulerFactory.get().call();
}
/**
* Returns the current thread local {@link ConnectionProvider}.
*
* @return
*/
ConnectionProvider connectionProvider() {
if (currentConnectionProvider.get() == null)
return cp;
else
return currentConnectionProvider.get();
}
/**
* Sets the current thread local {@link ConnectionProvider} to a singleton
* manual commit instance.
*/
void beginTransactionObserve() {
log.debug("beginTransactionObserve");
currentConnectionProvider.set(new ConnectionProviderSingletonManualCommit(cp));
if (isTransactionOpen.get() != null && isTransactionOpen.get())
throw new RuntimeException("cannot begin transaction as transaction open already");
isTransactionOpen.set(true);
}
/**
* Sets the current thread local {@link Scheduler} to be
* {@link Schedulers#trampoline()}.
*/
void beginTransactionSubscribe() {
log.debug("beginTransactionSubscribe");
currentSchedulerFactory.set(CURRENT_THREAD_SCHEDULER_FACTORY);
}
/**
* Resets the current thread local {@link Scheduler} to default.
*/
void endTransactionSubscribe() {
log.debug("endTransactionSubscribe");
currentSchedulerFactory.set(null);
}
/**
* Resets the current thread local {@link ConnectionProvider} to default.
*/
void endTransactionObserve() {
log.debug("endTransactionObserve");
currentConnectionProvider.set(cp);
isTransactionOpen.set(false);
}
/**
* Returns an {@link Operator} that performs commit or rollback of a
* transaction.
*
* @param isCommit
* @return
*/
private Operator commitOrRollbackOnCompleteOperator(final boolean isCommit) {
return RxUtil.toOperator(new Func1, Observable>() {
@Override
public Observable call(Observable source) {
return commitOrRollbackOnCompleteOperatorIfAtLeastOneValue(isCommit, Database.this,
source);
}
});
}
/**
* Commits current transaction on the completion of source if and only if
* the source sequence is non-empty.
*
* @return operator that commits on completion of source.
*/
public Operator commitOnCompleteOperator() {
return commitOrRollbackOnCompleteOperator(true);
}
/**
* Rolls back current transaction on the completion of source if and only if
* the source sequence is non-empty.
*
* @return operator that rolls back on completion of source.
*/
public Operator rollbackOnCompleteOperator() {
return commitOrRollbackOnCompleteOperator(false);
}
/**
* Starts a database transaction for each onNext call. Following database
* calls will be subscribed on current thread (Schedulers.trampoline()) and
* share the same {@link Connection} until transaction is rolled back or
* committed.
*
* @return begin transaction operator
*/
public Operator beginTransactionOnNextOperator() {
return RxUtil.toOperator(new Func1, Observable>() {
@Override
public Observable call(Observable source) {
return beginTransactionOnNext(Database.this, source);
}
});
}
/**
* Commits the currently open transaction. Emits true.
*
* @return
*/
public Operator commitOnNextOperator() {
return commitOrRollbackOnNextOperator(true);
}
public Operator> commitOnNextListOperator() {
return commitOrRollbackOnNextListOperator(true);
}
public Operator> rollbackOnNextListOperator() {
return commitOrRollbackOnNextListOperator(false);
}
private Operator> commitOrRollbackOnNextListOperator(
final boolean isCommit) {
return RxUtil.toOperator(new Func1>, Observable>() {
@Override
public Observable call(Observable> source) {
return source.concatMap(new Func1, Observable>() {
@Override
public Observable call(Observable source) {
if (isCommit)
return commit(source);
else
return rollback(source);
}
});
}
});
}
/**
* Rolls back the current transaction. Emits false.
*
* @return
*/
public Operator rollbackOnNextOperator() {
return commitOrRollbackOnNextOperator(false);
}
private Operator commitOrRollbackOnNextOperator(final boolean isCommit) {
return RxUtil.toOperator(new Func1, Observable>() {
@Override
public Observable call(Observable source) {
return commitOrRollbackOnNext(isCommit, Database.this, source);
}
});
}
private static Observable commitOrRollbackOnCompleteOperatorIfAtLeastOneValue(
final boolean isCommit, final Database db, Observable source) {
CountingAction counter = RxUtil.counter();
Observable commit = counter
// get count
.count()
// greater than zero or empty
.filter(greaterThanZero())
// commit if at least one value
.lift(db.commitOrRollbackOperator(isCommit));
return Observable
// concatenate
.concat(source
// count emissions
.doOnNext(counter)
// ignore emissions
.ignoreElements()
// cast the empty sequence to type Boolean
.cast(Boolean.class),
// concat with commit
commit);
}
/**
* Emits true for commit and false for rollback.
*
* @param isCommit
* @param db
* @param source
* @return
*/
private static Observable commitOrRollbackOnNext(final boolean isCommit,
final Database db, Observable source) {
return source.concatMap(new Func1>() {
@Override
public Observable call(T t) {
if (isCommit)
return db.commit();
else
return db.rollback();
}
});
}
private static Observable beginTransactionOnNext(final Database db, Observable source) {
return source.concatMap(new Func1>() {
@Override
public Observable call(T t) {
return db.beginTransaction().map(constant(t));
}
});
}
/**
* Returns an {@link Observable} that is the result of running a sequence of
* update commands (insert/update/delete, ddl) read from the given
* {@link Observable} sequence.
*
* @param commands
* @return
*/
public Observable run(Observable commands) {
return commands.reduce(Observable. empty(),
new Func2, String, Observable>() {
@Override
public Observable call(Observable dep, String command) {
return update(command).dependsOn(dep).count();
}
}).lift(RxUtil. flatten());
}
/**
* Returns an {@link Operator} version of {@link #run(Observable)}.
*
* @return
*/
public Operator run() {
return RxUtil.toOperator(new Func1, Observable>() {
@Override
public Observable call(Observable commands) {
return run(commands);
}
});
}
/**
* Returns an {@link Observable} that is the result of running a sequence of
* update commands (insert/update/delete, ddl) commands read from an
* InputStream using the given delimiter as the statement delimiter (for
* example semicolon).
*
* @param is
* @param delimiter
* @return
*/
public Observable run(InputStream is, String delimiter) {
return run(is, Charset.defaultCharset(), delimiter);
}
/**
* Returns an {@link Observable} that is the result of running a sequence of
* update commands (insert/update/delete, ddl) commands read from an
* {@link InputStream} with the given {@link Charset} using the given delimiter as the statement delimiter (for
* example semicolon).
*
* @param is
* @param delimiter
* @return
*/
public Observable run(InputStream is, Charset charset, String delimiter) {
return StringObservable.split(StringObservable.from(new InputStreamReader(is,charset)), ";").lift(
run());
}
/**
* Returns a Database based on the current Database except all queries run
* {@link Schedulers#io}.
*
* @return
*/
public Database asynchronous() {
return new Database(cp, IO_SCHEDULER_FACTORY);
}
/**
* Sentinel object used to indicate in parameters of a query that rather
* than calling {@link PreparedStatement#setObject(int, Object)} with a null
* we call {@link PreparedStatement#setNull(int, int)} with
* {@link Types#CLOB}. This is required by many databases for setting CLOB
* and BLOB fields to null.
*/
public static final Object NULL_CLOB = new Object();
public static Object toSentinelIfNull(String s) {
if (s == null)
return NULL_CLOB;
else
return s;
}
/**
* Sentinel object used to indicate in parameters of a query that rather
* than calling {@link PreparedStatement#setObject(int, Object)} with a null
* we call {@link PreparedStatement#setNull(int, int)} with
* {@link Types#CLOB}. This is required by many databases for setting CLOB
* and BLOB fields to null.
*/
public static final Object NULL_BLOB = new Object();
public static Object toSentinelIfNull(byte[] bytes) {
if (bytes == null)
return NULL_BLOB;
else
return bytes;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy