ch.inftec.ju.db.JuEmUtil Maven / Gradle / Ivy
Show all versions of ju-dbutil Show documentation
package ch.inftec.ju.db;
import java.io.PrintWriter;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
import org.hibernate.Session;
import org.hibernate.ejb.EntityManagerImpl;
import org.hibernate.ejb.HibernateEntityManagerFactory;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.jdbc.Work;
import org.hibernate.service.jdbc.connections.internal.DatasourceConnectionProviderImpl;
import org.hibernate.service.jdbc.connections.spi.ConnectionProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
import ch.inftec.ju.db.JuConnUtil.DbType;
import ch.inftec.ju.util.DataHolder;
/**
* Helper class building on EntityManager that provides DB utility functions.
*
* This class won't handle disposing of EntityManager. It won't handle transactions either. These things
* should either be handled by a container or by the JuEmfUtil class.
* @author Martin
*
*/
public class JuEmUtil implements DbWorker {
private final Logger logger = LoggerFactory.getLogger(JuEmUtil.class);
private final EntityManager em;
/**
* We'll cache the DbType to avoid transaction problems in certain cases (Liquibase log executions) as we
* need a running transaction to evaluate the DbType.
*/
private DbType dbType;
/**
* Creates a new JuDbUtil based on the specified EntityManager instance.
*
* Make sure the client takes care of EntityManager disposing
* @param em EntityManager instance
*/
public JuEmUtil(EntityManager em) {
this.em = em;
}
/**
* Gets the EntityManager wrapped by this util instance.
* @return EntityManager
*/
public EntityManager getEm() {
return this.em;
}
/**
* Returns a JuConnUtil that wraps this JuEmUtil instance.
*
* JuConnUtil will use the internal "raw" Connection object of the EntityManager
* to access the DB.
* @return JuConnUtil
*/
public JuConnUtil asConnUtil() {
return JuConnUtils.createByDbWorker(this);
}
@Override
public void doWork(final DbWork work) {
this.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
work.execute(connection);
}
});
}
/**
* Executes some DB work using a raw JDBC connection.
*
* Makes use of the Hibernate Work facility to extract a Connection from an EntityManager.
* Note that this connection will participate in the same transactional context as the EntityManager.
* @param work Work callback interface
*/
public void doWork(Work work) {
// Flush the EntityManager to make sure we have all entities available
// this.em.flush(); // Was causing errors while having cascade
Session session = this.em.unwrap(Session.class);
session.doWork(work);
}
/**
* Executes some DB work that require a DataSource instance.
*
* If we cannot get a DataSource from the connection provider, we'll just provide a
* dummy DataSource that returns the connection extracted from the EntityManager as
* in doWork(Work).
* @param work DsWork callback interface
*/
public void doWork(DsWork work) {
// Flush the EntityManager to make sure we have all entities available
//this.em.flush();
HibernateEntityManagerFactory factory = (HibernateEntityManagerFactory) this.em.unwrap(EntityManagerImpl.class).getEntityManagerFactory();
SessionFactoryImpl sessionFactory = (SessionFactoryImpl) factory.getSessionFactory();
ConnectionProvider connProvider = sessionFactory.getConnectionProvider();
DataSource ds = null;
if (connProvider instanceof DatasourceConnectionProviderImpl) {
ds = ((DatasourceConnectionProviderImpl) connProvider).getDataSource();
logger.debug("Using actual DataSource to execute DataSource work: " + ds);
} else {
ds = new ConnectionProviderDataSource(connProvider);
logger.debug("Using ConnectionProviderDataSource to execute DataSource work.");
}
work.execute(ds);
}
/**
* Gets the specified JpaRepository interface.
* @param repositoryClass Repository interface class
* @return JPA repository interface
*/
public T getJpaRepository(Class repositoryClass) {
JpaRepositoryFactory repositoryFactory = new JpaRepositoryFactory(this.em);
return repositoryFactory.getRepository(repositoryClass);
}
/**
* Method to extract info from the DatabaseMetaData.
* @param action Callback method to do the data extracting into an arbitrary data structure.
* @return Data as returned by the callback function
*/
public T extractDatabaseMetaData(final DatabaseMetaDataCallback action) {
final DataHolder res = new DataHolder<>();
this.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
DatabaseMetaData metaData = connection.getMetaData();
res.setValue(action.processMetaData(metaData));
}
});
return res.getValue();
}
/**
* Gets the Connection URL of this EntityManager from the MetaData.
* @return Connection URL
*/
public String getMetaDataUrl() {
return this.extractDatabaseMetaData(new DatabaseMetaDataCallback() {
@Override
public String processMetaData(DatabaseMetaData dbmd)
throws SQLException {
return dbmd.getURL();
}
});
}
/**
* Gets the UserName from the connection of this EntityManager from the MetaData.
* @return Connection UserName
*/
public String getMetaDataUserName() {
return this.extractDatabaseMetaData(new DatabaseMetaDataCallback() {
@Override
public String processMetaData(DatabaseMetaData dbmd)
throws SQLException {
return dbmd.getUserName();
}
});
}
/**
* Gets the name of the connection catalog or null if there is none.
* @return Catalog name
*/
public String getConnectionCatalog() {
final DataHolder catalog = new DataHolder<>();
this.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
catalog.setValue(connection.getCatalog());
}
});
return catalog.getValue();
}
/**
* Gets a list of all table names of the DB. Table names are returned the way the DB driver
* returns them, which may be lower, mixed or upper case.
* @return List of Table names
* @throws JuDbException If the list cannot be evaluated
*/
public List getTableNames() throws JuDbException {
List tableNames = this.extractDatabaseMetaData(new DatabaseMetaDataCallback>() {
@Override
public List processMetaData(DatabaseMetaData dbmd) throws SQLException {
// TODO: Consider Schema names for other DBs; refactor
String schemaName = null;
if (getDbType() == DbType.ORACLE) {
schemaName = getMetaDataUserName();
}
ResultSet rs = dbmd.getTables(schemaName, schemaName, null, new String[]{"TABLE"});
List tableNames = new ArrayList<>();
while (rs.next()) {
String tableName = rs.getString("TABLE_NAME");
// We check if the TableName already exists in the list as
// Oracle seems to return the same table names multiple times on some
// Schemas...
if (!tableNames.contains(tableName)) {
tableNames.add(tableName);
}
}
rs.close();
Collections.sort(tableNames);
return tableNames;
}
});
return tableNames;
}
/**
* Gets a list of the primery key columns for the specified table
*
* Column names are kept the way the driver returns them (may be upper, lower or mixed case)
* @param tableName Table name
* @return List of all columns that make up the primary key. If no primary key is applied, an empty list is returned.
*/
public List getPrimaryKeyColumns(final String tableName) {
final String actualTableName = this.getDbType().getDbSpecificHandler(this.asConnUtil()).convertTableNameCasing(tableName);
List columnNames = this.extractDatabaseMetaData(new DatabaseMetaDataCallback>() {
@Override
public List processMetaData(DatabaseMetaData dbmd) throws SQLException {
ResultSet rs = dbmd.getPrimaryKeys(null, null, actualTableName);
List columnNames = new ArrayList<>();
while (rs.next()) {
columnNames.add(rs.getString("COLUMN_NAME"));
}
rs.close();
return columnNames;
}
});
return columnNames;
}
/**
* Gets a list of all sequence names of the DB. Sequences names are returned the
* way the DB driver returns them, which is usually upper case.
* @return List of Sequences names
* @throws JuDbException If the list cannot be evaluated
*/
public List getSequenceNames() throws JuDbException {
// There doesn't seem to be a generic way to retrieve sequences using JDBC meta data, so
// we have to use native queries. The queries are stored in the DB specific handlers.
return this.getDbType().getDbSpecificHandler(this.asConnUtil()).getSequenceNames();
}
public Long getNextValueFromSequence(String sequenceName) throws JuDbException {
return this.getDbType().getDbSpecificHandler(this.asConnUtil()).getNextValueFromSequence(sequenceName);
}
/**
* Resets identity generation of all tables or sequences to allow for predictable
* and repeatable entity generation.
*
* The exact behaviour of identity generation may vary from DB to DB.
* @param val Value for the next primary key
*/
public void resetIdentityGenerationOrSequences(int val) {
this.getDbType().getDbSpecificHandler(this.asConnUtil()).resetIdentityGenerationOrSequences(val);
}
// /**
// * Sets the nextVal of an Oracle sequence.
// *
// * Only works with sequences that have an increment of +1.
// * @param sequenceName Sequence name
// * @param nextVal Value that should be yielded by next NEXVAL call
// */
// public void oracleSequenceSetNextVal(String sequenceName, long nextVal) {
// Long currentValue = ((BigDecimal) this.em.createNativeQuery(String.format("SELECT %s.NEXTVAL from dual", sequenceName)).getSingleResult()).longValue();
// logger.debug(String.format("Restarting Sequence %s (currentVal=%d) with %d", sequenceName, currentValue, nextVal));
//
// Long increment = nextVal - currentValue.longValue() - 1;
// this.em.createNativeQuery(String.format("ALTER SEQUENCE %s INCREMENT BY %d", sequenceName, increment)).executeUpdate();
// this.em.createNativeQuery(String.format("SELECT %s.NEXTVAL from dual", sequenceName)).executeUpdate();
// this.em.createNativeQuery(String.format("ALTER SEQUENCE %s INCREMENT BY 1", sequenceName)).executeUpdate();
// }
/**
* Gets the type of the DB implementation of this EntityManager. If the type is not known (or supported)
* by JuEmUtil, an exception is thrown.
* @return DbType
*/
public DbType getDbType() {
if (this.dbType == null) {
String productName = this.extractDatabaseMetaData(new DatabaseMetaDataCallback() {
@Override
public String processMetaData(DatabaseMetaData dbmd) throws SQLException {
return dbmd.getDatabaseProductName();
}
});
this.dbType = DbType.evaluateDbType(productName);
}
return this.dbType;
}
private static class ConnectionProviderDataSource implements DataSource {
private final ConnectionProvider connectionProvider;
public ConnectionProviderDataSource(ConnectionProvider connProvider) {
this.connectionProvider = connProvider;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
throw new UnsupportedOperationException("unwrap");
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
throw new UnsupportedOperationException("unwrap");
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
throw new UnsupportedOperationException("unwrap");
}
@Override
public int getLoginTimeout() throws SQLException {
throw new UnsupportedOperationException("unwrap");
}
@Override
public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
throw new UnsupportedOperationException("unwrap");
}
@Override
public T unwrap(Class iface) throws SQLException {
throw new UnsupportedOperationException("unwrap");
}
@Override
public boolean isWrapperFor(Class> iface) throws SQLException {
throw new UnsupportedOperationException("unwrap");
}
@Override
public Connection getConnection() throws SQLException {
Connection conn = this.connectionProvider.getConnection();
Connection connProxy = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), new Class[] { Connection.class}, new ConnectionInvocationHandler(conn));
return connProxy;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
throw new UnsupportedOperationException("unwrap");
}
private class ConnectionInvocationHandler implements InvocationHandler {
private final Connection wrappedConnection;
private ConnectionInvocationHandler(Connection wrappedConnection) {
this.wrappedConnection = wrappedConnection;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("close")) {
connectionProvider.closeConnection(this.wrappedConnection);
return null;
} else {
return method.invoke(this.wrappedConnection, args);
}
}
}
}
}