org.rapidoid.jdbc.JdbcClient Maven / Gradle / Ivy
The newest version!
/*-
* #%L
* rapidoid-sql
* %%
* Copyright (C) 2014 - 2018 Nikolche Mihajlovski and contributors
* %%
* Licensed 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.
* #L%
*/
package org.rapidoid.jdbc;
import org.rapidoid.annotation.Authors;
import org.rapidoid.annotation.Since;
import org.rapidoid.cls.Cls;
import org.rapidoid.collection.Coll;
import org.rapidoid.concurrent.Callback;
import org.rapidoid.concurrent.Callbacks;
import org.rapidoid.config.Conf;
import org.rapidoid.config.Config;
import org.rapidoid.datamodel.Results;
import org.rapidoid.datamodel.impl.ResultsImpl;
import org.rapidoid.group.AutoManageable;
import org.rapidoid.group.ManageableBean;
import org.rapidoid.io.Res;
import org.rapidoid.lambda.Mapper;
import org.rapidoid.lambda.Operation;
import org.rapidoid.log.Log;
import org.rapidoid.u.U;
import org.rapidoid.util.LazyInit;
import org.rapidoid.util.Msc;
import org.rapidoid.util.MscOpts;
import javax.sql.DataSource;
import java.sql.*;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
@Authors("Nikolche Mihajlovski")
@Since("3.0.0")
@ManageableBean(kind = "jdbc")
public class JdbcClient extends AutoManageable {
private static final String DEFAULT_POOL_PROVIDER = "hikari";
private volatile boolean initialized;
private volatile String username;
private volatile String password;
private volatile String driver;
private volatile String url;
private volatile boolean usePool = true;
private volatile DataSource dataSource;
private volatile String poolProvider = DEFAULT_POOL_PROVIDER;
private volatile ReadWriteMode mode = ReadWriteMode.READ_WRITE;
private final Config config;
private final LazyInit workers = new LazyInit<>(new Callable() {
@Override
public JdbcWorkers call() throws Exception {
return new JdbcWorkers(JdbcClient.this);
}
});
public JdbcClient(String name) {
super(name);
this.config = Conf.JDBC.defaultOrCustom(name);
configure();
}
public void configure() {
url(config.entry("url").str().getOrNull());
username(config.entry("username").str().getOrNull());
password(config.entry("password").str().getOrNull());
driver(config.entry("driver").str().getOrNull());
poolProvider(config.entry("poolProvider").or(DEFAULT_POOL_PROVIDER));
if (U.isEmpty(driver) && U.notEmpty(url)) {
driver(inferDriverFromUrl(url));
}
}
private static String inferDriverFromUrl(String url) {
if (url.startsWith("jdbc:mysql:")) {
return "com.mysql.jdbc.Driver";
} else if (url.startsWith("jdbc:h2:")) {
return "org.hibernate.dialect.H2Dialect";
} else if (url.startsWith("jdbc:hsqldb:")) {
return "org.hsqldb.jdbc.JDBCDriver";
}
return null;
}
public synchronized JdbcClient username(String username) {
if (U.neq(this.username, username)) {
this.username = username;
this.initialized = false;
}
return this;
}
public synchronized JdbcClient password(String password) {
if (U.neq(this.password, password)) {
this.password = password;
this.initialized = false;
}
return this;
}
public synchronized JdbcClient driver(String driver) {
if (U.neq(this.driver, driver)) {
this.driver = driver;
this.initialized = false;
}
return this;
}
/**
* Use dataSource(...) instead.
*/
@Deprecated
public synchronized JdbcClient pool(DataSource pool) {
return dataSource(pool);
}
public synchronized JdbcClient dataSource(DataSource dataSource) {
if (U.neq(this.dataSource, dataSource)) {
this.dataSource = dataSource;
this.usePool = dataSource != null;
this.initialized = false;
}
return this;
}
public synchronized JdbcClient url(String url) {
if (U.neq(this.url, url)) {
this.url = url;
this.initialized = false;
}
return this;
}
public synchronized JdbcClient usePool(boolean usePool) {
if (U.neq(this.usePool, usePool)) {
this.usePool = usePool;
this.initialized = false;
}
return this;
}
public synchronized JdbcClient mode(ReadWriteMode mode) {
if (U.neq(this.mode, mode)) {
this.mode = mode;
this.initialized = false;
}
return this;
}
public JdbcClient poolProvider(String poolProvider) {
if (U.neq(this.poolProvider, poolProvider)) {
this.poolProvider = poolProvider;
this.initialized = false;
}
return this;
}
/**
* Use usePool(true)
instead.
*/
@Deprecated
public JdbcClient pooled() {
usePool(true);
return this;
}
public JdbcClient mysql(String host, int port, String databaseName) {
return driver("com.mysql.jdbc.Driver").url(U.frmt("jdbc:mysql://%s:%s/%s", host, port, databaseName));
}
public JdbcClient h2(String databaseName) {
return driver("org.h2.Driver").url("jdbc:h2:mem:" + databaseName + ";DB_CLOSE_DELAY=-1").username("sa").password("");
}
public JdbcClient hsql(String databaseName) {
return driver("org.hsqldb.jdbc.JDBCDriver").url("jdbc:hsqldb:mem:" + databaseName).username("sa").password("");
}
private void registerJDBCDriver() {
if (driver == null && url != null) {
driver = inferDriverFromUrl(url);
}
validateArgNotNull("driver", driver);
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
throw U.rte("Cannot find JDBC driver class: " + driver);
}
}
private void validateArgNotNull(String argName, String argValue) {
if (argValue == null) {
throw U.rte("The JDBC parameter '" + argName + "' must be configured!");
}
}
private synchronized void ensureIsInitialized() {
if (!initialized) {
validate();
registerJDBCDriver();
if (this.dataSource == null) {
this.dataSource = this.usePool ? createPool() : null;
}
String ds = dataSource != null ? Cls.of(dataSource).getSimpleName() : null;
Log.info("Initialized JDBC API", "!url", url, "!driver", driver, "!username", username, "!password", maskedPassword(), "!dataSource", ds);
initialized = true;
}
}
private String maskedPassword() {
return U.isEmpty(password) ? "" : "";
}
private DataSource createPool() {
String provider = U.safe(poolProvider);
switch (provider) {
case "hikari":
U.must(MscOpts.hasHikari(), "Couldn't find Hikari!");
Log.info("Initializing JDBC connection pool with Hikari", "!url", url, "!driver", driver, "!username", username, "!password", maskedPassword());
return HikariFactory.createDataSourceFor(this);
case "c3p0":
U.must(MscOpts.hasC3P0(), "Couldn't find C3P0!");
Log.info("Initializing JDBC connection pool with C3P0", "!url", url, "!driver", driver, "!username", username, "!password", maskedPassword());
return C3P0Factory.createDataSourceFor(this);
default:
throw U.rte("Unknown pool provider: '%s'!", provider);
}
}
private void validate() {
U.must(U.notEmpty(username != null), "The database username must be specified!");
U.must(U.notEmpty(password != null), "The database password must be specified!");
U.must(U.notEmpty(url != null), "The database connection URL must be specified!");
U.must(U.notEmpty(driver != null), "The database driver must be specified!");
}
public Connection getConnection() {
ensureIsInitialized();
return provideConnection();
}
private static void close(Connection conn) {
try {
if (conn != null) conn.close();
} catch (SQLException e) {
throw U.rte("Error occurred while closing the connection!", e);
}
}
private static void close(PreparedStatement stmt) {
try {
if (stmt != null) stmt.close();
} catch (SQLException e) {
throw U.rte("Error occurred while closing the statement!", e);
}
}
private static void close(ResultSet rs) {
try {
if (rs != null) rs.close();
} catch (SQLException e) {
throw U.rte("Error occurred while closing the ResultSet!", e);
}
}
public int execute(String sql, Object... args) {
return doExecute(sql, null, args);
}
public int execute(String sql, Map namedArgs) {
return doExecute(sql, namedArgs, null);
}
private int doExecute(String sql, Map namedArgs, Object[] args) {
ensureIsInitialized();
sql = toSql(sql);
Log.debug("SQL", "sql", sql, "args", args);
try (Connection conn = provideConnection();
PreparedStatement stmt = JDBC.prepare(conn, sql, namedArgs, args)) {
String q = sql.trim().toUpperCase();
if (q.startsWith("INSERT ")
|| q.startsWith("UPDATE ")
|| q.startsWith("DELETE ")) {
return stmt.executeUpdate();
} else {
return stmt.execute() ? 1 : 0;
}
} catch (SQLException e) {
throw U.rte(e);
}
}
public int tryToExecute(String sql, Object... args) {
return doTryToExecute(sql, null, args);
}
public int tryToExecute(String sql, Map namedArgs) {
return doTryToExecute(sql, namedArgs, null);
}
private int doTryToExecute(String sql, Map namedArgs, Object[] args) {
try {
return doExecute(sql, namedArgs, args);
} catch (Exception e) {
// ignore the exception
Log.warn("Ignoring JDBC error", "error", Msc.errorMsg(e));
}
return 0;
}
public Results query(Class resultType, String sql, Object... args) {
return doQuery(resultType, null, sql, null, args);
}
public Results query(Class resultType, String sql, Map namedArgs) {
return doQuery(resultType, null, sql, namedArgs, null);
}
public Results query(Mapper resultMapper, String sql, Object... args) {
return doQuery(null, resultMapper, sql, null, args);
}
public Results query(Mapper resultMapper, String sql, Map namedArgs) {
return doQuery(null, resultMapper, sql, namedArgs, null);
}
private Results doQuery(Class resultType, Mapper resultMapper,
String sql, Map namedArgs, Object[] args) {
sql = toSql(sql);
JdbcData data = new JdbcData<>(this, resultType, resultMapper, sql, namedArgs, args);
return new ResultsImpl<>(data);
}
List runQuery(Class resultType, Mapper resultMapper,
String sql, Map namedArgs, Object[] args,
long skip, int limit) {
ensureIsInitialized();
boolean needsPaging = skip > 0 || (limit >= 0 && limit < Integer.MAX_VALUE);
if (limit == -1) {
// unlimited
limit = Integer.MAX_VALUE;
}
U.must(skip >= 0);
U.must(limit >= 0);
boolean pagingInSql = sql.contains("$skip") || sql.contains("$limit");
if (pagingInSql) {
sql = replacePagingParams(sql, skip, limit);
}
List results = fetchData(resultType, resultMapper, sql, namedArgs, args);
if (needsPaging && !pagingInSql) {
results = Coll.range(results, Msc.toInt(skip), Msc.toInt(skip + limit));
}
U.must(results.size() <= limit, "Paging error: too many results!");
return results;
}
private String replacePagingParams(String sql, long skip, int limit) {
return sql
.replace("$skip", skip + "")
.replace("$limit", limit + "");
}
private List fetchData(Class resultType, Mapper resultMapper,
String sql, Map namedArgs, Object[] args) {
try (Connection conn = provideConnection();
PreparedStatement stmt = JDBC.prepare(conn, sql, namedArgs, args);
ResultSet rs = stmt.executeQuery()) {
if (resultMapper != null) {
return JDBC.rows(resultMapper, rs);
} else {
if (resultType.equals(Map.class)) {
return U.cast(JDBC.rows(rs));
} else {
return JDBC.rows(resultType, rs);
}
}
} catch (Exception e) {
throw U.rte(e);
}
}
long getQueryCount(String sql, Map namedArgs, Object[] args) {
// FIXME find a better way
return -1; // unknown
}
private static String toSql(String sql) {
if (sql.endsWith(".sql")) {
sql = Res.from(sql).mustExist().getContent();
}
return sql;
}
public Results