net.freeutils.util.db.DBManager Maven / Gradle / Ivy
Show all versions of jelementary Show documentation
/*
* Copyright © 2003-2024 Amichai Rothman
*
* This file is part of JElementary - the Java Elementary Utilities package.
*
* JElementary is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* JElementary is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with JElementary. If not, see .
*
* For additional info see https://www.freeutils.net/source/jelementary/
*/
package net.freeutils.util.db;
import java.io.Closeable;
import java.sql.*;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import javax.sql.DataSource;
import net.freeutils.util.Containers;
import net.freeutils.util.Reflect;
import net.freeutils.util.config.*;
/**
* The {@code DBManager} class provides database management functionality.
*
* A sample usage (ignoring proper error handling) might look like the following:
*
* // configure and open connection
* DBManager db = new DBManager();
* db.configure(new Configuration() {{
* // for MySQL
* set("dataSourceClass", "com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource");
* set("user", "myUser");
* set("password", "myPassword");
* set("serverName", "localhost");
* set("databaseName", "myDB");
* set("autoCommit", "false");
* }});
* db.open();
*
* // run non-transactional queries
* User user = db.runner().queryBean(User.class, "SELECT * FROM users WHERE id = ?", userId);
*
* // run transactional queries
* DBRunner runner = db.transactionRunner();
* try {
* runner.update("DELETE FROM users WHERE id = ?", userId);
* runner.update("DELETE FROM friends WHERE user_id = ?", userId);
* runner.update("DELETE FROM friends WHERE friend_id = ?", userId);
* runner.commit();
* } catch (SQLException sqle) {
* runner.rollback();
* throw sqle;
* } finally {
* runner.close();
* }
*
* // close db
* db.close();
*
*
* @author Yaniv Kunda
* @author Amichai Rothman
* @since 2009-01-14
*/
public class DBManager implements Configurable, Closeable {
protected volatile String dataSourceClassName;
protected volatile Map defaultAttributes;
protected volatile int maxConnections;
protected volatile long connectionTimeout;
protected volatile DBConnectionPool pool;
/**
* Creates a new data source of the given class name with the given attributes.
*
* Attributes are set by calling their corresponding setter methods on the data source
*
* @param dataSourceClassName the data source class name
* @param attributes the attributes to set in the data source, by invoking
* the corresponding setter methods
* @return the newly created data source
* @throws SQLException if an error occurs
*/
protected static DataSource createDataSource(String dataSourceClassName, Map attributes) throws SQLException {
DataSource dataSource;
try {
dataSource = Reflect.newInstance(dataSourceClassName);
} catch (Exception e) {
throw new SQLException("can't create datasource " + dataSourceClassName, e);
}
Reflect.setProperties(dataSource, attributes);
return dataSource;
}
/**
* Configures the DBManager using the given configuration section.
*
* The configuration properties used by this class are the
* dataSourceClass, user, password and maxConnections properties.
*
* Other properties are vendor-specific and passed transparently to its
* DataSource instance using setters.
*
* For details on which DataSource class name to specify for your JDBC driver,
* and which configuration properties it supports, see the vendor's documentation.
*
* Note that connection URL properties are often, but not always, equivalent to
* DataSource properties, so you should specifically search for vendor documentation
* on DataSource properties rather than connection URL properties if possible.
*
* @param conf the configuration section containing the configuration data
* @throws Exception if an error occurs which prevents the DBManager from being configured
*/
@Override
public void configure(Configuration conf) throws Exception {
dataSourceClassName = conf.get("dataSourceClass");
defaultAttributes = conf.strings();
// validate mandatory attributes
if (!defaultAttributes.containsKey("user") || !defaultAttributes.containsKey("password"))
throw new ConfigurationException("missing user/password");
maxConnections = conf.getInt("maxConnections", 25);
connectionTimeout = conf.getLong("connectionTimeout", 10000L);
}
/**
* Opens the data source and initializes the connection pool.
*
* @throws SQLException if an error occurs
* @throws IllegalStateException if this manager has not been configured
*/
public void open() throws SQLException {
if (dataSourceClassName == null)
throw new IllegalStateException("DBManager is not configured");
if (pool != null)
throw new IllegalStateException("DBManager is already open");
DataSource dataSource = createDataSource(dataSourceClassName, defaultAttributes);
pool = new DBConnectionPool(dataSource, connectionTimeout, maxConnections);
// test connection
Connection conn = getConnection();
try {
testConnection(conn);
} finally {
conn.close();
}
}
/**
* Closes all connections and releases all resources.
* This instance must not be used after it is closed.
*/
@Override
public void close() {
if (pool != null)
pool.close();
pool = null;
}
/**
* Returns a non-pooled connection with the given attributes added to the default attributes.
*
* Attributes are set by calling their corresponding setter methods on the data source.
*
* The caller must close the connection when done using it.
*
* This method is useful for databases that require passing special connection
* attributes to perform one-time operations such as creating or destroying a database.
*
* @param attributes the attributes to add to the default attributes, ordered as
* name1, value1, name2, value2, ... , nameN, valueN
* @return a connection
* @throws SQLException if an error occurs
*/
public Connection getConnection(String... attributes) throws SQLException {
if (defaultAttributes == null)
throw new IllegalStateException("DBManager has not been configured");
Map finalAttributes = new LinkedHashMap<>(defaultAttributes);
Containers.putAll(finalAttributes, (Object[])attributes);
return createDataSource(dataSourceClassName, finalAttributes).getConnection();
}
/**
* Retrieves a pooled connection with the default attributes.
*
* Note: in order for the connection to be returned to the pool
* it must be {@link java.sql.Connection#close() closed}, for example:
*
* Connection conn = db.getConnection();
* try {
* // run queries...
* } finally {
* conn.close();
* }
*
*
* @return a pooled connection
* @throws SQLException if an error occurs
*/
public Connection getConnection() throws SQLException {
if (defaultAttributes == null)
throw new IllegalStateException("DBManager has not been configured");
if (pool == null)
throw new IllegalStateException("DBManager is closed");
try {
return pool.getConnection();
} catch (InterruptedException ie) {
throw new SQLException("interrupted while getting connection", ie);
} catch (TimeoutException te) {
throw new SQLException("timeout while getting connection", te);
}
}
/**
* Closes the given connection, silently ignoring any errors.
*
* @param conn the connection to close
*/
public static void closeSilently(Connection conn) {
try {
if (conn != null)
conn.close();
} catch (SQLException ignore) {}
}
/**
* Closes the given statement, silently ignoring any errors.
*
* @param statement the statement to close
*/
public static void closeSilently(Statement statement) {
try {
if (statement != null)
statement.close();
} catch (SQLException ignore) {}
}
/**
* Tests whether the connection is functional.
* If not, it must not be used for any further queries.
*
* @param conn the connection to test
* @return true if the connection is functional, false if not
*/
public static boolean testConnection(Connection conn) {
try {
return !conn.isClosed() && conn.isValid(0);
} catch (Throwable t) {
return false;
}
}
/**
* Returns a DBRunner on which queries can be run. The runner will retain a
* single connection for the transaction, and must be committed/rolled back
* and closed by caller.
*
* Subclasses can override this method to return a custom DBRunner subclass.
*
* @return a new DBRunner
* @throws SQLException if an error occurs
*/
public DBRunner transactionRunner() throws SQLException {
return new DBRunner(getConnection());
}
/**
* Returns a DBRunner on which queries can be run. Each query will be run possibly
* on a different connection, updates will be committed automatically, and no
* resources will be held in between queries.
*
* Subclasses can override this method to return a custom DBRunner subclass.
*
* @return a new DBRunner
* @throws SQLException if an error occurs
*/
public DBRunner runner() throws SQLException {
return new DBRunner(this);
}
}