
dk.eobjects.metamodel.JdbcDataContextStrategy Maven / Gradle / Ivy
The newest version!
/**
* This file is part of MetaModel.
*
* MetaModel 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 3 of the License, or
* (at your option) any later version.
*
* MetaModel 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 MetaModel. If not, see .
*/
package dk.eobjects.metamodel;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import javax.sql.DataSource;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import dk.eobjects.metamodel.data.DataSet;
import dk.eobjects.metamodel.data.MaxRowsDataSetStrategyWrapper;
import dk.eobjects.metamodel.dialects.DefaultQueryRewriter;
import dk.eobjects.metamodel.dialects.IQueryRewriter;
import dk.eobjects.metamodel.dialects.MysqlQueryRewriter;
import dk.eobjects.metamodel.dialects.PostgresqlQueryRewriter;
import dk.eobjects.metamodel.dialects.SQLServerQueryRewriter;
import dk.eobjects.metamodel.query.Query;
import dk.eobjects.metamodel.query.SelectItem;
import dk.eobjects.metamodel.schema.Column;
import dk.eobjects.metamodel.schema.ColumnType;
import dk.eobjects.metamodel.schema.JdbcColumn;
import dk.eobjects.metamodel.schema.JdbcSchema;
import dk.eobjects.metamodel.schema.JdbcTable;
import dk.eobjects.metamodel.schema.Relationship;
import dk.eobjects.metamodel.schema.Schema;
import dk.eobjects.metamodel.schema.Table;
import dk.eobjects.metamodel.schema.TableType;
/**
* DataContextStrategy to use for JDBC-compliant databases
*/
public class JdbcDataContextStrategy implements IDataContextStrategy {
public static final String DATABASE_PRODUCT_POSTGRESQL = "PostgreSQL";
public static final String DATABASE_PRODUCT_MYSQL = "MySQL";
public static final String DATABASE_PRODUCT_HSQLDB = "HSQL Database Engine";
public static final String DATABASE_PRODUCT_SQLSERVER = "Microsoft SQL Server";
public static final int FETCH_SIZE = 25000;
private static final Log _log = LogFactory
.getLog(JdbcDataContextStrategy.class);
private Connection _connection;
private TableType[] _tableTypes;
private String _catalogName;
private DataSource _dataSource;
private String _databaseProductName;
private IQueryRewriter _queryRewriter = new DefaultQueryRewriter();
/**
* There are some confusion as to the definition of catalogs and schemas.
* Some databases seperate "groups of tables" by using schemas, others by
* catalogs. This variable indicates whether a MetaModel schema really
* represents a catalog.
*/
private boolean _usesCatalogsAsSchemas;
private String _identifierQuoteString;
/**
* Creates the strategy based on a data source, some table types and an
* optional catalogName
*
* @param dataSource
* the datasource objcet to use for making connections
* @param tableTypes
* the types of tables to include
* @param catalogName
* a catalog name to use, can be null
*/
public JdbcDataContextStrategy(DataSource dataSource,
TableType[] tableTypes, String catalogName) {
_dataSource = dataSource;
_tableTypes = tableTypes;
_catalogName = catalogName;
initialize();
}
/**
* Creates the strategy based on a connection, some table types and an
* optional catalogName
*
* @param connection
* the database connection
* @param tableTypes
* the types of tables to include
* @param catalogName
* a catalog name to use, can be null
*/
public JdbcDataContextStrategy(Connection connection,
TableType[] tableTypes, String catalogName) {
_connection = connection;
_tableTypes = tableTypes;
_catalogName = catalogName;
initialize();
}
private void initialize() {
try {
DatabaseMetaData metaData = getConnection().getMetaData();
try {
_identifierQuoteString = metaData.getIdentifierQuoteString();
} catch (SQLException e) {
_log.warn(e);
}
_usesCatalogsAsSchemas = usesCatalogsAsSchemas(metaData);
try {
_databaseProductName = metaData.getDatabaseProductName();
if (_log.isDebugEnabled()) {
_log
.debug("Database product name: "
+ _databaseProductName);
}
if (DATABASE_PRODUCT_MYSQL.equals(_databaseProductName)) {
setQueryRewriter(new MysqlQueryRewriter());
} else if (DATABASE_PRODUCT_POSTGRESQL
.equals(_databaseProductName)) {
setQueryRewriter(new PostgresqlQueryRewriter());
} else if (DATABASE_PRODUCT_SQLSERVER
.equals(_databaseProductName)) {
setQueryRewriter(new SQLServerQueryRewriter());
}
} catch (SQLException e) {
_log.warn("Could not retrieve database product name: "
+ e.getMessage());
}
} catch (SQLException e) {
_log.debug("initialize(): " + e.getMessage());
}
}
private boolean usesCatalogsAsSchemas(DatabaseMetaData metaData) {
boolean result = true;
ResultSet rs = null;
try {
rs = metaData.getSchemas();
while (rs.next() && result) {
result = false;
}
} catch (SQLException e) {
_log.warn("Could not retrieve schema and catalog information: "
+ e.getMessage());
result = false;
} finally {
close(rs, null);
}
return result;
}
private void loadTables(JdbcSchema schema, DatabaseMetaData metaData,
String[] types) {
ResultSet rs = null;
try {
if (_log.isInfoEnabled()) {
_log.info("Querying for table types "
+ ArrayUtils.toString(types) + " in catalog: "
+ _catalogName + ", schema: " + schema.getName());
}
if (_usesCatalogsAsSchemas) {
rs = metaData.getTables(schema.getName(), null, null, types);
} else {
rs = metaData.getTables(_catalogName, schema.getName(), null,
types);
}
while (rs.next()) {
String tableCatalog = rs.getString(1);
String tableSchema = rs.getString(2);
String tableName = rs.getString(3);
String tableTypeName = rs.getString(4);
TableType tableType = TableType.getTableType(tableTypeName);
String tableRemarks = rs.getString(5);
if (_log.isDebugEnabled()) {
_log.debug("Found table: tableCatalog=" + tableCatalog
+ ",tableSchema=" + tableSchema + ",tableName="
+ tableName);
}
if (tableSchema == null) {
tableSchema = tableCatalog;
}
JdbcTable table = new JdbcTable(tableName, tableType, schema,
this);
table.setRemarks(tableRemarks);
schema.addTable(table);
}
} catch (SQLException e) {
_log.error("Error retriving table metadata", e);
} finally {
close(rs, null);
}
}
/**
* Loads index metadata for a table
*
* @param table
*/
public void loadIndexes(JdbcTable table) {
try {
DatabaseMetaData metaData = getConnection().getMetaData();
loadIndexes(table, metaData);
} catch (Exception e) {
_log.error(e);
}
}
private void loadIndexes(Table table, DatabaseMetaData metaData) {
Schema schema = table.getSchema();
ResultSet rs = null;
// Ticket #170: IndexInfo is nice-to-have, not need-to-have, so
// we will do a nice failover on SQLExceptions
try {
if (_usesCatalogsAsSchemas) {
rs = metaData.getIndexInfo(schema.getName(), null, table
.getName(), false, true);
} else {
rs = metaData.getIndexInfo(_catalogName, schema.getName(),
table.getName(), false, true);
}
while (rs.next()) {
String columnName = rs.getString(9);
if (columnName != null) {
Column column = table.getColumnByName(columnName);
if (column != null) {
column.setIndexed(true);
} else {
_log.error("Indexed column \"" + columnName
+ "\" could not be found in table: " + table);
}
}
}
} catch (SQLException e) {
_log.warn("Could not retrieve index info for table: "
+ table.getName());
if (_log.isDebugEnabled()) {
_log.debug(e);
} else if (_log.isInfoEnabled()) {
_log.info(e.getMessage());
}
} finally {
close(rs, null);
}
}
/**
* Loads column metadata (no indexes though) for a table
*
* @param table
*/
public void loadColumns(JdbcTable table) {
try {
DatabaseMetaData metaData = getConnection().getMetaData();
loadColumns(table, metaData);
} catch (Exception e) {
_log.error(e);
}
}
private void loadColumns(JdbcTable table, DatabaseMetaData metaData) {
Schema schema = table.getSchema();
ResultSet rs = null;
try {
if (_log.isInfoEnabled()) {
_log.info("Querying for columns in table: " + table.getName());
}
table.setQuote(_identifierQuoteString);
int columnNumber = -1;
if (_usesCatalogsAsSchemas) {
rs = metaData.getColumns(schema.getName(), null, table
.getName(), null);
} else {
rs = metaData.getColumns(_catalogName, schema.getName(), table
.getName(), null);
}
while (rs.next()) {
columnNumber++;
String columnName = rs.getString(4);
if (_identifierQuoteString == null
&& new StringTokenizer(columnName).countTokens() > 1) {
_log.warn("column name contains whitespace: \""
+ columnName + "\".");
}
int jdbcType = rs.getInt(5);
ColumnType columnType = ColumnType.convertColumnType(jdbcType);
String nativeType = rs.getString(6);
Integer columnSize = rs.getInt(7);
int jdbcNullable = rs.getInt(11);
Boolean nullable = null;
if (jdbcNullable == DatabaseMetaData.columnNullable) {
nullable = true;
} else if (jdbcNullable == DatabaseMetaData.columnNoNulls) {
nullable = false;
}
String remarks = rs.getString(12);
Column column = new JdbcColumn(columnName, columnType, table,
columnNumber, nullable);
column.setRemarks(remarks);
column.setNativeType(nativeType);
column.setColumnSize(columnSize);
column.setQuote(_identifierQuoteString);
table.addColumn(column);
}
} catch (SQLException e) {
_log.error("Error retriving table metadata", e);
} finally {
close(rs, null);
}
}
public void loadRelations(Schema schema) {
Table[] tables = schema.getTables();
try {
DatabaseMetaData metaData = getConnection().getMetaData();
for (Table table : tables) {
loadRelations(table, metaData);
}
} catch (Exception e) {
_log.error(e);
}
}
private void loadRelations(Table table, DatabaseMetaData metaData) {
Schema schema = table.getSchema();
ResultSet rs = null;
try {
if (_usesCatalogsAsSchemas) {
rs = metaData.getImportedKeys(schema.getName(), null, table
.getName());
} else {
rs = metaData.getImportedKeys(_catalogName, schema.getName(),
table.getName());
}
loadRelations(rs, schema);
} catch (SQLException e) {
_log.error("Error retriving table metadata", e);
} finally {
close(rs, null);
}
}
private void loadRelations(ResultSet rs, Schema schema) throws SQLException {
while (rs.next()) {
String pkTableName = rs.getString(3);
String pkColumnName = rs.getString(4);
Column pkColumn = null;
Table pkTable = schema.getTableByName(pkTableName);
if (pkTable != null) {
pkColumn = pkTable.getColumnByName(pkColumnName);
}
if (_log.isDebugEnabled()) {
_log.debug("Found primary key relation: tableName="
+ pkTableName + ",columnName=" + pkColumnName
+ ", matching column: " + pkColumn);
}
String fkTableName = rs.getString(7);
String fkColumnName = rs.getString(8);
Column fkColumn = null;
Table fkTable = schema.getTableByName(fkTableName);
if (fkTable != null) {
fkColumn = fkTable.getColumnByName(fkColumnName);
}
if (_log.isDebugEnabled()) {
_log.debug("Found foreign key relation: tableName="
+ fkTableName + ",columnName=" + fkColumnName
+ ", matching column: " + fkColumn);
}
if (pkColumn == null || fkColumn == null) {
_log.error("Could not find relation columns: pkTableName="
+ pkTableName + ",pkColumnName=" + pkColumnName
+ "fkTableName=" + fkTableName + ",fkColumnName="
+ fkColumnName);
_log.error("pkColumn=" + pkColumn);
_log.error("fkColumn=" + fkColumn);
} else {
Relationship[] relations = pkTable.getRelationships(fkTable);
boolean exists = false;
for (Relationship relation : relations) {
if (relation.containsColumnPair(pkColumn, fkColumn)) {
exists = true;
break;
}
}
if (!exists) {
Relationship.createRelationship(new Column[] { pkColumn },
new Column[] { fkColumn });
}
}
}
}
public DataSet executeQuery(Query query) throws MetaModelException {
Statement statement = null;
ResultSet resultSet = null;
try {
String queryString;
if (_queryRewriter != null) {
queryString = _queryRewriter.rewriteQuery(this, query);
} else {
queryString = query.toString();
}
Connection connection = getConnection();
statement = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY);
try {
statement.setFetchSize(FETCH_SIZE);
} catch (Exception e) {
// Ticket #372: Sometimes an exception is thrown here even
// though it's contrary to the jdbc spec. We'll proceed without
// doing anything about it though.
_log.warn("Could not setFetchSize on statement: "
+ e.getMessage());
}
boolean postProcessMaxRows = false;
Integer maxRows = query.getMaxRows();
if (maxRows != null) {
try {
statement.setMaxRows(maxRows);
} catch (SQLException e) {
_log.debug("setMaxRows(" + maxRows + ") was rejected.", e);
postProcessMaxRows = true;
}
}
_log.info(queryString);
resultSet = statement.executeQuery(queryString);
DataSet dataSet = new DataSet(query, statement, resultSet);
if (postProcessMaxRows) {
dataSet = new DataSet(new MaxRowsDataSetStrategyWrapper(
dataSet, maxRows));
}
return dataSet;
} catch (SQLException e) {
_log.warn("Exception occurred when executing query: "
+ e.getMessage());
close(resultSet, statement);
if (isComplexQuery(query)) {
_log
.info("Trying to query again with failover strategy, this may be a performance problem.");
IDataContextStrategy failoverStrategy = getFailoverStrategy(getConnection());
return failoverStrategy.executeQuery(query);
} else {
throw new MetaModelException("Could not execute query", e);
}
}
}
/**
* Determines if a query is of a complex type which may be error-prone to
* execute on some databases and therefore should be tried to execute with a
* query postprocessing strategy
*/
private boolean isComplexQuery(Query q) {
if (q.getWhereClause().getItemCount() > 0) {
return true;
}
if (q.getGroupByClause().getItemCount() > 0) {
return true;
}
if (q.getHavingClause().getItemCount() > 0) {
return true;
}
if (q.getOrderByClause().getItemCount() > 0) {
return true;
}
return false;
}
/**
* Creates a IDataContextStrategy that uses a more fault-tolerant
* query-postprocessing strategy.
*/
private IDataContextStrategy getFailoverStrategy(final Connection con) {
IDataContextStrategy failoverStrategy = new QueryPostprocessDataContextStrategy() {
@Override
public DataSet materializeMainSchemaTable(Table table,
Column[] columns, int maxRows) {
Statement statement = null;
ResultSet resultSet = null;
try {
SelectItem[] selectItems = new SelectItem[columns.length];
statement = con.createStatement();
StringBuilder sb = new StringBuilder("SELECT ");
for (int i = 0; i < columns.length; i++) {
Column column = columns[i];
selectItems[i] = new SelectItem(column);
if (i != 0) {
sb.append(", ");
}
sb.append(columns[i].getQuotedName());
}
sb.append(" FROM ");
if (table.getSchema() != null
&& table.getSchema().getName() != null) {
sb.append(table.getSchema().getName() + '.');
}
sb.append(table.getQuotedName());
String queryString = sb.toString();
_log.info("Using failover query: " + queryString);
if (maxRows != -1) {
statement.setMaxRows(maxRows);
}
resultSet = statement.executeQuery(queryString);
DataSet dataSet = new DataSet(selectItems, statement,
resultSet);
return dataSet;
} catch (SQLException e) {
close(resultSet, statement);
throw new MetaModelException(e);
}
}
@Override
protected Schema getMainSchema() throws MetaModelException {
return JdbcDataContextStrategy.this
.getSchemaByName(getMainSchemaName());
}
@Override
protected String getMainSchemaName() throws MetaModelException {
return JdbcDataContextStrategy.this.getDefaultSchemaName();
}
};
return failoverStrategy;
}
/**
* Quietly closes any of the parameterized JDBC objects
*
* @param rs
* @param st
*/
public static void close(ResultSet rs, Statement st) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
_log.info("Could not close ResultSet", e);
}
}
if (st != null) {
try {
st.close();
} catch (SQLException e) {
_log.info("Could not close Statement", e);
}
}
}
/**
* Convenience method to get the available catalogNames using this
* connection.
*
* @return a String-array with the names of the available catalogs.
*/
public String[] getCatalogNames() {
// Retrieve metadata
DatabaseMetaData metaData = null;
ResultSet rs = null;
try {
metaData = _connection.getMetaData();
} catch (SQLException e) {
_log.error("Error retrieving metadata", e);
throw new MetaModelException(e);
}
// Retrieve catalogs
_log.info("Retrieving catalogs");
List catalogs = new ArrayList();
try {
rs = metaData.getCatalogs();
while (rs.next()) {
String catalogName = rs.getString(1);
_log.debug("Found catalogName: " + catalogName);
catalogs.add(catalogName);
}
} catch (SQLException e) {
_log.error("Error retrieving catalog metadata", e);
} finally {
close(rs, null);
_log.info("Retrieved " + catalogs.size() + " catalogs");
}
return catalogs.toArray(new String[catalogs.size()]);
}
/**
* Gets an appropriate connection object to use - either a dedicated
* connection or a new connection from the datasource object
*/
private Connection getConnection() {
if (_dataSource == null) {
return _connection;
}
try {
return _dataSource.getConnection();
} catch (SQLException e) {
throw new MetaModelException("Could not establish connection", e);
}
}
public String getDefaultSchemaName() {
// Use a boolean to check if the result has been
// found, because a schema name can actually be
// null (for example in the case of Firebird
// databases).
boolean found = false;
String result = null;
try {
String[] schemaNames = getSchemaNames();
// First strategy: If there's only one schema available, that must
// be it
if (schemaNames.length == 1) {
result = schemaNames[0];
found = true;
}
if (!found) {
Connection connection = getConnection();
DatabaseMetaData metaData = connection.getMetaData();
// Second strategy: Find default schema name by examining the
// URL
if (!found) {
String url = metaData.getURL();
if (url != null && url.length() > 0) {
if (schemaNames.length > 0) {
StringTokenizer st = new StringTokenizer(url,
"/\\:");
int tokenCount = st.countTokens();
if (tokenCount > 0) {
for (int i = 1; i < tokenCount; i++) {
st.nextToken();
}
String lastToken = st.nextToken();
for (int i = 0; i < schemaNames.length
&& !found; i++) {
String schemaName = schemaNames[i];
if (lastToken.indexOf(schemaName) != -1) {
result = schemaName;
found = true;
}
}
}
}
}
}
// Third strategy: Check for schema equal to username
if (!found) {
String username = metaData.getUserName();
if (username != null) {
for (int i = 0; i < schemaNames.length && !found; i++) {
if (username.equalsIgnoreCase(schemaNames[i])) {
result = schemaNames[i];
found = true;
}
}
}
}
// Fourth strategy: Find default schema name by vendor-specific
// hacks
if (!found) {
if (DATABASE_PRODUCT_POSTGRESQL
.equalsIgnoreCase(_databaseProductName)) {
if (_catalogName == null) {
result = "public";
} else {
result = _catalogName;
}
found = true;
}
if (DATABASE_PRODUCT_HSQLDB
.equalsIgnoreCase(_databaseProductName)) {
for (int i = 0; i < schemaNames.length && !found; i++) {
String schemaName = schemaNames[i];
if ("PUBLIC".equals(schemaName)) {
result = schemaName;
found = true;
}
}
}
}
}
} catch (SQLException e) {
_log.error("Could not determine default schema name: "
+ e.getMessage());
}
return result;
}
public Schema getSchemaByName(String name) throws MetaModelException {
DatabaseMetaData metaData;
try {
metaData = getConnection().getMetaData();
} catch (SQLException e) {
throw new MetaModelException(e);
}
JdbcSchema schema = new JdbcSchema(name, this);
// Creates string array to represent the table types
String[] types = getTableTypeAsString();
loadTables(schema, metaData, types);
return schema;
}
public String[] getSchemaNames() throws MetaModelException {
try {
DatabaseMetaData metaData = getConnection().getMetaData();
Collection result = new ArrayList();
if (DATABASE_PRODUCT_SQLSERVER.equals(_databaseProductName)) {
result = getSchemaSQLServerNames();
} else if (_usesCatalogsAsSchemas) {
String[] catalogNames = getCatalogNames();
for (String name : catalogNames) {
_log.info("Found catalogName: " + name);
result.add(name);
}
} else {
ResultSet rs = metaData.getSchemas();
while (rs.next()) {
String schemaName = rs.getString(1);
_log.info("Found schemaName: " + schemaName);
result.add(schemaName);
}
rs.close();
}
if (DATABASE_PRODUCT_MYSQL.equals(_databaseProductName)) {
result.remove("information_schema");
}
// If still no schemas are found, add a schema with a null-name
if (result.isEmpty()) {
_log
.info("No schemas or catalogs found. Creating unnamed schema.");
result.add(null);
}
return result.toArray(new String[result.size()]);
} catch (SQLException e) {
throw new MetaModelException(e);
}
}
/**
* Microsoft SQL Server returns users instead of schemas when calling
* metadata.getSchemas() This is a simple workaround.
*
* @return
* @throws SQLException
*/
private Set getSchemaSQLServerNames() throws SQLException {
// Distinct schema names. metaData.getTables() is a denormalized
// resultset
Set schemas = new HashSet();
DatabaseMetaData metaData = getConnection().getMetaData();
ResultSet rs = metaData.getTables(_catalogName, null, null,
getTableTypeAsString());
while (rs.next()) {
schemas.add(rs.getString("TABLE_SCHEM"));
}
return schemas;
}
private String[] getTableTypeAsString() {
String[] types = new String[_tableTypes.length];
for (int i = 0; i < types.length; i++) {
if (_tableTypes[i] == TableType.OTHER) {
// if the OTHER type has been selected, don't use a table
// pattern (ie. include all types)
types = null;
break;
}
types[i] = _tableTypes[i].toString();
}
return types;
}
public JdbcDataContextStrategy setQueryRewriter(IQueryRewriter queryRewriter) {
_queryRewriter = queryRewriter;
return this;
}
public IQueryRewriter getQueryRewriter() {
return _queryRewriter;
}
public String getIdentifierQuoteString() {
return _identifierQuoteString;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy