Java tutorial
package org.apache.ddlutils.platform; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. */ import java.io.IOException; import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigDecimal; import java.sql.BatchUpdateException; import java.sql.Blob; import java.sql.Clob; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.Statement; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.commons.beanutils.DynaBean; import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.Predicate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.ddlutils.DatabaseOperationException; import org.apache.ddlutils.DdlUtilsException; import org.apache.ddlutils.Platform; import org.apache.ddlutils.PlatformInfo; import org.apache.ddlutils.alteration.AddColumnChange; import org.apache.ddlutils.alteration.AddForeignKeyChange; import org.apache.ddlutils.alteration.AddIndexChange; import org.apache.ddlutils.alteration.AddPrimaryKeyChange; import org.apache.ddlutils.alteration.AddTableChange; import org.apache.ddlutils.alteration.ColumnDefinitionChange; import org.apache.ddlutils.alteration.ColumnOrderChange; import org.apache.ddlutils.alteration.ForeignKeyChange; import org.apache.ddlutils.alteration.IndexChange; import org.apache.ddlutils.alteration.ModelChange; import org.apache.ddlutils.alteration.ModelComparator; import org.apache.ddlutils.alteration.PrimaryKeyChange; import org.apache.ddlutils.alteration.RecreateTableChange; import org.apache.ddlutils.alteration.RemoveColumnChange; import org.apache.ddlutils.alteration.RemoveForeignKeyChange; import org.apache.ddlutils.alteration.RemoveIndexChange; import org.apache.ddlutils.alteration.RemovePrimaryKeyChange; import org.apache.ddlutils.alteration.RemoveTableChange; import org.apache.ddlutils.alteration.TableChange; import org.apache.ddlutils.alteration.TableDefinitionChangesPredicate; import org.apache.ddlutils.dynabean.SqlDynaClass; import org.apache.ddlutils.dynabean.SqlDynaProperty; import org.apache.ddlutils.model.CloneHelper; import org.apache.ddlutils.model.Column; import org.apache.ddlutils.model.Database; import org.apache.ddlutils.model.ForeignKey; import org.apache.ddlutils.model.Index; import org.apache.ddlutils.model.ModelException; import org.apache.ddlutils.model.Table; import org.apache.ddlutils.model.TypeMap; import org.apache.ddlutils.util.JdbcSupport; import org.apache.ddlutils.util.SqlTokenizer; /** * Base class for platform implementations. * * @version $Revision: 231110 $ */ public abstract class PlatformImplBase extends JdbcSupport implements Platform { /** The default name for models read from the database, if no name as given.*/ protected static final String MODEL_DEFAULT_NAME = "default"; /** The log for this platform. */ private final Log _log = LogFactory.getLog(getClass()); /** The platform info. */ private PlatformInfo _info = new PlatformInfo(); /** The sql builder for this platform. */ private SqlBuilder _builder; /** The model reader for this platform. */ private JdbcModelReader _modelReader; /** Whether script mode is on. */ private boolean _scriptModeOn; /** Whether SQL comments are generated or not. */ private boolean _sqlCommentsOn = true; /** Whether delimited identifiers are used or not. */ private boolean _delimitedIdentifierModeOn; /** Whether identity override is enabled. */ private boolean _identityOverrideOn; /** Whether read foreign keys shall be sorted alphabetically. */ private boolean _foreignKeysSorted; /** Whether to use the default ON UPDATE action if the specified one is unsupported. */ private boolean _useDefaultOnUpdateActionIfUnsupported = true; /** Whether to use the default ON DELETE action if the specified one is unsupported. */ private boolean _useDefaultOnDeleteActionIfUnsupported = true; /** * {@inheritDoc} */ public SqlBuilder getSqlBuilder() { return _builder; } /** * Sets the sql builder for this platform. * * @param builder The sql builder */ protected void setSqlBuilder(SqlBuilder builder) { _builder = builder; } /** * {@inheritDoc} */ public JdbcModelReader getModelReader() { if (_modelReader == null) { _modelReader = new JdbcModelReader(this); } return _modelReader; } /** * Sets the model reader for this platform. * * @param modelReader The model reader */ protected void setModelReader(JdbcModelReader modelReader) { _modelReader = modelReader; } /** * {@inheritDoc} */ public PlatformInfo getPlatformInfo() { return _info; } /** * {@inheritDoc} */ public boolean isScriptModeOn() { return _scriptModeOn; } /** * {@inheritDoc} */ public void setScriptModeOn(boolean scriptModeOn) { _scriptModeOn = scriptModeOn; } /** * {@inheritDoc} */ public boolean isSqlCommentsOn() { return _sqlCommentsOn; } /** * {@inheritDoc} */ public void setSqlCommentsOn(boolean sqlCommentsOn) { if (!getPlatformInfo().isSqlCommentsSupported() && sqlCommentsOn) { throw new DdlUtilsException("Platform " + getName() + " does not support SQL comments"); } _sqlCommentsOn = sqlCommentsOn; } /** * {@inheritDoc} */ public boolean isDelimitedIdentifierModeOn() { return _delimitedIdentifierModeOn; } /** * {@inheritDoc} */ public void setDelimitedIdentifierModeOn(boolean delimitedIdentifierModeOn) { if (!getPlatformInfo().isDelimitedIdentifiersSupported() && delimitedIdentifierModeOn) { throw new DdlUtilsException("Platform " + getName() + " does not support delimited identifier"); } _delimitedIdentifierModeOn = delimitedIdentifierModeOn; } /** * {@inheritDoc} */ public boolean isIdentityOverrideOn() { return _identityOverrideOn; } /** * {@inheritDoc} */ public void setIdentityOverrideOn(boolean identityOverrideOn) { _identityOverrideOn = identityOverrideOn; } /** * {@inheritDoc} */ public boolean isForeignKeysSorted() { return _foreignKeysSorted; } /** * {@inheritDoc} */ public void setForeignKeysSorted(boolean foreignKeysSorted) { _foreignKeysSorted = foreignKeysSorted; } /** * {@inheritDoc} */ public boolean isDefaultOnUpdateActionUsedIfUnsupported() { return _useDefaultOnUpdateActionIfUnsupported; } /** * {@inheritDoc} */ public void setDefaultOnUpdateActionUsedIfUnsupported(boolean useDefault) { _useDefaultOnUpdateActionIfUnsupported = useDefault; } /** * {@inheritDoc} */ public boolean isDefaultOnDeleteActionUsedIfUnsupported() { return _useDefaultOnDeleteActionIfUnsupported; } /** * {@inheritDoc} */ public void setDefaultOnDeleteActionUsedIfUnsupported(boolean useDefault) { _useDefaultOnDeleteActionIfUnsupported = useDefault; } /** * Returns the log for this platform. * * @return The log */ protected Log getLog() { return _log; } /** * Logs any warnings associated to the given connection. Note that the connection needs * to be open for this. * * @param connection The open connection */ protected void logWarnings(Connection connection) throws SQLException { SQLWarning warning = connection.getWarnings(); while (warning != null) { getLog().warn(warning.getLocalizedMessage(), warning.getCause()); warning = warning.getNextWarning(); } } /** * {@inheritDoc} */ public int evaluateBatch(String sql, boolean continueOnError) throws DatabaseOperationException { Connection connection = borrowConnection(); try { return evaluateBatch(connection, sql, continueOnError); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public int evaluateBatch(Connection connection, String sql, boolean continueOnError) throws DatabaseOperationException { Statement statement = null; int errors = 0; int commandCount = 0; // we tokenize the SQL along the delimiters, and we also make sure that only delimiters // at the end of a line or the end of the string are used (row mode) try { statement = connection.createStatement(); SqlTokenizer tokenizer = new SqlTokenizer(sql); while (tokenizer.hasMoreStatements()) { String command = tokenizer.getNextStatement(); // ignore whitespace command = command.trim(); if (command.length() == 0) { continue; } commandCount++; if (_log.isDebugEnabled()) { _log.debug("About to execute SQL " + command); } try { int results = statement.executeUpdate(command); if (_log.isDebugEnabled()) { _log.debug("After execution, " + results + " row(s) have been changed"); } } catch (SQLException ex) { if (continueOnError) { // Since the user deciced to ignore this error, we log the error // on level warn, and the exception itself on level debug _log.warn("SQL Command " + command + " failed with: " + ex.getMessage()); if (_log.isDebugEnabled()) { _log.debug(ex); } errors++; } else { throw new DatabaseOperationException("Error while executing SQL " + command, ex); } } // lets display any warnings SQLWarning warning = connection.getWarnings(); while (warning != null) { _log.warn(warning.toString()); warning = warning.getNextWarning(); } connection.clearWarnings(); } _log.info("Executed " + commandCount + " SQL command(s) with " + errors + " error(s)"); } catch (SQLException ex) { throw new DatabaseOperationException("Error while executing SQL", ex); } finally { closeStatement(statement); } return errors; } /** * {@inheritDoc} */ public void shutdownDatabase() throws DatabaseOperationException { Connection connection = borrowConnection(); try { shutdownDatabase(connection); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public void shutdownDatabase(Connection connection) throws DatabaseOperationException { // Per default do nothing as most databases don't need this } /** * {@inheritDoc} */ public void createDatabase(String jdbcDriverClassName, String connectionUrl, String username, String password, Map parameters) throws DatabaseOperationException, UnsupportedOperationException { throw new UnsupportedOperationException( "Database creation is not supported for the database platform " + getName()); } /** * {@inheritDoc} */ public void dropDatabase(String jdbcDriverClassName, String connectionUrl, String username, String password) throws DatabaseOperationException, UnsupportedOperationException { throw new UnsupportedOperationException( "Database deletion is not supported for the database platform " + getName()); } /** * {@inheritDoc} */ public void createTables(Database model, boolean dropTablesFirst, boolean continueOnError) throws DatabaseOperationException { createModel(model, dropTablesFirst, continueOnError); } /** * {@inheritDoc} */ public void createTables(Database model, CreationParameters params, boolean dropTablesFirst, boolean continueOnError) throws DatabaseOperationException { createModel(model, params, dropTablesFirst, continueOnError); } /** * {@inheritDoc} */ public void createTables(Connection connection, Database model, boolean dropTablesFirst, boolean continueOnError) throws DatabaseOperationException { createModel(connection, model, dropTablesFirst, continueOnError); } /** * {@inheritDoc} */ public void createTables(Connection connection, Database model, CreationParameters params, boolean dropTablesFirst, boolean continueOnError) throws DatabaseOperationException { createModel(connection, model, params, dropTablesFirst, continueOnError); } /** * {@inheritDoc} */ public String getCreateTablesSql(Database model, boolean dropTablesFirst, boolean continueOnError) { return getCreateModelSql(model, dropTablesFirst, continueOnError); } /** * {@inheritDoc} */ public String getCreateTablesSql(Database model, CreationParameters params, boolean dropTablesFirst, boolean continueOnError) { return getCreateModelSql(model, params, dropTablesFirst, continueOnError); } /** * {@inheritDoc} */ public void createModel(Database model, boolean dropTablesFirst, boolean continueOnError) throws DatabaseOperationException { Connection connection = borrowConnection(); try { createModel(connection, model, dropTablesFirst, continueOnError); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public void createModel(Connection connection, Database model, boolean dropTablesFirst, boolean continueOnError) throws DatabaseOperationException { String sql = getCreateModelSql(model, dropTablesFirst, continueOnError); evaluateBatch(connection, sql, continueOnError); } /** * {@inheritDoc} */ public void createModel(Database model, CreationParameters params, boolean dropTablesFirst, boolean continueOnError) throws DatabaseOperationException { Connection connection = borrowConnection(); try { createModel(connection, model, params, dropTablesFirst, continueOnError); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public void createModel(Connection connection, Database model, CreationParameters params, boolean dropTablesFirst, boolean continueOnError) throws DatabaseOperationException { String sql = getCreateModelSql(model, params, dropTablesFirst, continueOnError); evaluateBatch(connection, sql, continueOnError); } /** * {@inheritDoc} */ public String getCreateModelSql(Database model, boolean dropTablesFirst, boolean continueOnError) { String sql = null; try { StringWriter buffer = new StringWriter(); getSqlBuilder().setWriter(buffer); getSqlBuilder().createTables(model, dropTablesFirst); sql = buffer.toString(); } catch (IOException e) { // won't happen because we're using a string writer } return sql; } /** * {@inheritDoc} */ public String getCreateModelSql(Database model, CreationParameters params, boolean dropTablesFirst, boolean continueOnError) { String sql = null; try { StringWriter buffer = new StringWriter(); getSqlBuilder().setWriter(buffer); getSqlBuilder().createTables(model, params, dropTablesFirst); sql = buffer.toString(); } catch (IOException e) { // won't happen because we're using a string writer } return sql; } /** * Returns the model comparator to be used for this platform. This method is intendeded * to be redefined by platforms that need to customize the model reader. * * @return The model comparator */ protected ModelComparator getModelComparator() { return new ModelComparator(getPlatformInfo(), getTableDefinitionChangesPredicate(), isDelimitedIdentifierModeOn()); } /** * Returns the predicate that defines which changes are supported by the platform. * * @return The predicate */ protected TableDefinitionChangesPredicate getTableDefinitionChangesPredicate() { return new DefaultTableDefinitionChangesPredicate(); } /** * {@inheritDoc} */ public List getChanges(Database currentModel, Database desiredModel) { List changes = getModelComparator().compare(currentModel, desiredModel); return sortChanges(changes); } /** * Sorts the changes so that they can be executed by the database. E.g. tables need to be created before * they can be referenced by foreign keys, indexes should be dropped before a table is dropped etc. * * @param changes The original changes * @return The sorted changes - this can be the original list object or a new one */ protected List sortChanges(List changes) { final Map typeOrder = new HashMap(); typeOrder.put(RemoveForeignKeyChange.class, new Integer(0)); typeOrder.put(RemoveIndexChange.class, new Integer(1)); typeOrder.put(RemoveTableChange.class, new Integer(2)); typeOrder.put(RecreateTableChange.class, new Integer(3)); typeOrder.put(RemovePrimaryKeyChange.class, new Integer(3)); typeOrder.put(RemoveColumnChange.class, new Integer(4)); typeOrder.put(ColumnDefinitionChange.class, new Integer(5)); typeOrder.put(ColumnOrderChange.class, new Integer(5)); typeOrder.put(AddColumnChange.class, new Integer(5)); typeOrder.put(PrimaryKeyChange.class, new Integer(5)); typeOrder.put(AddPrimaryKeyChange.class, new Integer(6)); typeOrder.put(AddTableChange.class, new Integer(7)); typeOrder.put(AddIndexChange.class, new Integer(8)); typeOrder.put(AddForeignKeyChange.class, new Integer(9)); Collections.sort(changes, new Comparator() { public int compare(Object objA, Object objB) { Integer orderValueA = (Integer) typeOrder.get(objA.getClass()); Integer orderValueB = (Integer) typeOrder.get(objB.getClass()); if (orderValueA == null) { return (orderValueB == null ? 0 : 1); } else if (orderValueB == null) { return -1; } else { return orderValueA.compareTo(orderValueB); } } }); return changes; } /** * {@inheritDoc} */ public void alterTables(Database desiredModel, boolean continueOnError) throws DatabaseOperationException { Connection connection = borrowConnection(); try { Database currentModel = readModelFromDatabase(connection, desiredModel.getName()); alterModel(currentModel, desiredModel, continueOnError); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public void alterTables(Database desiredModel, CreationParameters params, boolean continueOnError) throws DatabaseOperationException { Connection connection = borrowConnection(); try { Database currentModel = readModelFromDatabase(connection, desiredModel.getName()); alterModel(currentModel, desiredModel, params, continueOnError); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public void alterTables(String catalog, String schema, String[] tableTypes, Database desiredModel, boolean continueOnError) throws DatabaseOperationException { Connection connection = borrowConnection(); try { Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema, tableTypes); alterModel(currentModel, desiredModel, continueOnError); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public void alterTables(String catalog, String schema, String[] tableTypes, Database desiredModel, CreationParameters params, boolean continueOnError) throws DatabaseOperationException { Connection connection = borrowConnection(); try { Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema, tableTypes); alterModel(currentModel, desiredModel, params, continueOnError); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public void alterTables(Connection connection, Database desiredModel, boolean continueOnError) throws DatabaseOperationException { Database currentModel = readModelFromDatabase(connection, desiredModel.getName()); alterModel(currentModel, desiredModel, continueOnError); } /** * {@inheritDoc} */ public void alterTables(Connection connection, Database desiredModel, CreationParameters params, boolean continueOnError) throws DatabaseOperationException { Database currentModel = readModelFromDatabase(connection, desiredModel.getName()); alterModel(currentModel, desiredModel, params, continueOnError); } /** * {@inheritDoc} */ public void alterTables(Connection connection, String catalog, String schema, String[] tableTypes, Database desiredModel, boolean continueOnError) throws DatabaseOperationException { Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema, tableTypes); alterModel(currentModel, desiredModel, continueOnError); } /** * {@inheritDoc} */ public void alterTables(Connection connection, String catalog, String schema, String[] tableTypes, Database desiredModel, CreationParameters params, boolean continueOnError) throws DatabaseOperationException { Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema, tableTypes); alterModel(currentModel, desiredModel, params, continueOnError); } /** * {@inheritDoc} */ public String getAlterTablesSql(Database desiredModel) throws DatabaseOperationException { Connection connection = borrowConnection(); try { Database currentModel = readModelFromDatabase(connection, desiredModel.getName()); return getAlterModelSql(currentModel, desiredModel); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public String getAlterTablesSql(Database desiredModel, CreationParameters params) throws DatabaseOperationException { Connection connection = borrowConnection(); try { Database currentModel = readModelFromDatabase(connection, desiredModel.getName()); return getAlterModelSql(currentModel, desiredModel, params); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public String getAlterTablesSql(String catalog, String schema, String[] tableTypes, Database desiredModel) throws DatabaseOperationException { Connection connection = borrowConnection(); try { Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema, tableTypes); return getAlterModelSql(currentModel, desiredModel); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public String getAlterTablesSql(String catalog, String schema, String[] tableTypes, Database desiredModel, CreationParameters params) throws DatabaseOperationException { Connection connection = borrowConnection(); try { Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema, tableTypes); return getAlterModelSql(currentModel, desiredModel, params); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public String getAlterTablesSql(Connection connection, Database desiredModel) throws DatabaseOperationException { Database currentModel = readModelFromDatabase(connection, desiredModel.getName()); return getAlterModelSql(currentModel, desiredModel); } /** * {@inheritDoc} */ public String getAlterTablesSql(Connection connection, Database desiredModel, CreationParameters params) throws DatabaseOperationException { Database currentModel = readModelFromDatabase(connection, desiredModel.getName()); return getAlterModelSql(currentModel, desiredModel, params); } /** * {@inheritDoc} */ public String getAlterTablesSql(Connection connection, String catalog, String schema, String[] tableTypes, Database desiredModel) throws DatabaseOperationException { Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema, tableTypes); return getAlterModelSql(currentModel, desiredModel); } /** * {@inheritDoc} */ public String getAlterTablesSql(Connection connection, String catalog, String schema, String[] tableTypes, Database desiredModel, CreationParameters params) throws DatabaseOperationException { Database currentModel = readModelFromDatabase(connection, desiredModel.getName(), catalog, schema, tableTypes); return getAlterModelSql(currentModel, desiredModel, params); } /** * {@inheritDoc} */ public String getAlterModelSql(Database currentModel, Database desiredModel) throws DatabaseOperationException { return getAlterModelSql(currentModel, desiredModel, null); } /** * {@inheritDoc} */ public String getAlterModelSql(Database currentModel, Database desiredModel, CreationParameters params) throws DatabaseOperationException { List changes = getChanges(currentModel, desiredModel); String sql = null; try { StringWriter buffer = new StringWriter(); getSqlBuilder().setWriter(buffer); processChanges(currentModel, changes, params); sql = buffer.toString(); } catch (IOException ex) { // won't happen because we're using a string writer } return sql; } /** * {@inheritDoc} */ public void alterModel(Database currentModel, Database desiredModel, boolean continueOnError) throws DatabaseOperationException { Connection connection = borrowConnection(); try { alterModel(connection, currentModel, desiredModel, continueOnError); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public void alterModel(Database currentModel, Database desiredModel, CreationParameters params, boolean continueOnError) throws DatabaseOperationException { Connection connection = borrowConnection(); try { alterModel(connection, currentModel, desiredModel, params, continueOnError); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public void alterModel(Connection connection, Database currentModel, Database desiredModel, boolean continueOnError) throws DatabaseOperationException { String sql = getAlterModelSql(currentModel, desiredModel); evaluateBatch(connection, sql, continueOnError); } /** * {@inheritDoc} */ public void alterModel(Connection connection, Database currentModel, Database desiredModel, CreationParameters params, boolean continueOnError) throws DatabaseOperationException { String sql = getAlterModelSql(currentModel, desiredModel, params); evaluateBatch(connection, sql, continueOnError); } /** * {@inheritDoc} */ public void dropTable(Connection connection, Database model, Table table, boolean continueOnError) throws DatabaseOperationException { String sql = getDropTableSql(model, table, continueOnError); evaluateBatch(connection, sql, continueOnError); } /** * {@inheritDoc} */ public void dropTable(Database model, Table table, boolean continueOnError) throws DatabaseOperationException { Connection connection = borrowConnection(); try { dropTable(connection, model, table, continueOnError); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public String getDropTableSql(Database model, Table table, boolean continueOnError) { String sql = null; try { StringWriter buffer = new StringWriter(); getSqlBuilder().setWriter(buffer); getSqlBuilder().dropTable(model, table); sql = buffer.toString(); } catch (IOException e) { // won't happen because we're using a string writer } return sql; } /** * {@inheritDoc} */ public void dropTables(Database model, boolean continueOnError) throws DatabaseOperationException { dropModel(model, continueOnError); } /** * {@inheritDoc} */ public void dropTables(Connection connection, Database model, boolean continueOnError) throws DatabaseOperationException { dropModel(connection, model, continueOnError); } /** * {@inheritDoc} */ public String getDropTablesSql(Database model, boolean continueOnError) { return getDropModelSql(model); } /** * {@inheritDoc} */ public void dropModel(Database model, boolean continueOnError) throws DatabaseOperationException { Connection connection = borrowConnection(); try { dropModel(connection, model, continueOnError); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public void dropModel(Connection connection, Database model, boolean continueOnError) throws DatabaseOperationException { String sql = getDropModelSql(model); evaluateBatch(connection, sql, continueOnError); } /** * {@inheritDoc} */ public String getDropModelSql(Database model) { String sql = null; try { StringWriter buffer = new StringWriter(); getSqlBuilder().setWriter(buffer); getSqlBuilder().dropTables(model); sql = buffer.toString(); } catch (IOException e) { // won't happen because we're using a string writer } return sql; } /** * Processes the given changes in the specified order. Basically, this method finds the * appropriate handler method (one of the <code>processChange</code> methods) defined in * the concrete sql builder for each change, and invokes it. * * @param model The database model; this object is not going to be changed by this method * @param changes The changes * @param params The parameters used in the creation of new tables. Note that for existing * tables, the parameters won't be applied * @return The changed database model */ protected Database processChanges(Database model, Collection changes, CreationParameters params) throws IOException, DdlUtilsException { Database currentModel = new CloneHelper().clone(model); for (Iterator it = changes.iterator(); it.hasNext();) { invokeChangeHandler(currentModel, params, (ModelChange) it.next()); } return currentModel; } /** * Invokes the change handler (one of the <code>processChange</code> methods) for the given * change object. * * @param currentModel The current database schema * @param params The parameters used in the creation of new tables. Note that for existing * tables, the parameters won't be applied * @param change The change object */ private void invokeChangeHandler(Database currentModel, CreationParameters params, ModelChange change) throws IOException { Class curClass = getClass(); // find the handler for the change while ((curClass != null) && !Object.class.equals(curClass)) { try { Method method = null; try { method = curClass.getDeclaredMethod("processChange", new Class[] { Database.class, CreationParameters.class, change.getClass() }); } catch (NoSuchMethodException ex) { // we actually expect this one } if (method != null) { method.invoke(this, new Object[] { currentModel, params, change }); return; } else { curClass = curClass.getSuperclass(); } } catch (InvocationTargetException ex) { if (ex.getTargetException() instanceof IOException) { throw (IOException) ex.getTargetException(); } else { throw new DdlUtilsException(ex.getTargetException()); } } catch (Exception ex) { throw new DdlUtilsException(ex); } } throw new DdlUtilsException("No handler for change of type " + change.getClass().getName() + " defined"); } /** * Finds the table changed by the change object in the given model. * * @param currentModel The model to find the table in * @param change The table change * @return The table * @throws ModelException If the table could not be found */ protected Table findChangedTable(Database currentModel, TableChange change) throws ModelException { Table table = currentModel.findTable(change.getChangedTable(), getPlatformInfo().isDelimitedIdentifiersSupported()); if (table == null) { throw new ModelException("Could not find table " + change.getChangedTable() + " in the given model"); } else { return table; } } /** * Finds the index changed by the change object in the given model. * * @param currentModel The model to find the index in * @param change The index change * @return The index * @throws ModelException If the index could not be found */ protected Index findChangedIndex(Database currentModel, IndexChange change) throws ModelException { Index index = change.findChangedIndex(currentModel, getPlatformInfo().isDelimitedIdentifiersSupported()); if (index == null) { throw new ModelException("Could not find the index to change in table " + change.getChangedTable() + " in the given model"); } else { return index; } } /** * Finds the foreign key changed by the change object in the given model. * * @param currentModel The model to find the foreign key in * @param change The foreign key change * @return The foreign key * @throws ModelException If the foreign key could not be found */ protected ForeignKey findChangedForeignKey(Database currentModel, ForeignKeyChange change) throws ModelException { ForeignKey fk = change.findChangedForeignKey(currentModel, getPlatformInfo().isDelimitedIdentifiersSupported()); if (fk == null) { throw new ModelException("Could not find the foreign key to change in table " + change.getChangedTable() + " in the given model"); } else { return fk; } } /** * Processes a change representing the addition of a table. * * @param currentModel The current database schema * @param params The parameters used in the creation of new tables. Note that for existing * tables, the parameters won't be applied * @param change The change object */ public void processChange(Database currentModel, CreationParameters params, AddTableChange change) throws IOException { getSqlBuilder().createTable(currentModel, change.getNewTable(), params == null ? null : params.getParametersFor(change.getNewTable())); change.apply(currentModel, isDelimitedIdentifierModeOn()); } /** * Processes a change representing the removal of a table. * * @param currentModel The current database schema * @param params The parameters used in the creation of new tables. Note that for existing * tables, the parameters won't be applied * @param change The change object */ public void processChange(Database currentModel, CreationParameters params, RemoveTableChange change) throws IOException, ModelException { Table changedTable = findChangedTable(currentModel, change); getSqlBuilder().dropTable(changedTable); change.apply(currentModel, isDelimitedIdentifierModeOn()); } /** * Processes a change representing the addition of a foreign key. * * @param currentModel The current database schema * @param params The parameters used in the creation of new tables. Note that for existing * tables, the parameters won't be applied * @param change The change object */ public void processChange(Database currentModel, CreationParameters params, AddForeignKeyChange change) throws IOException { Table changedTable = findChangedTable(currentModel, change); getSqlBuilder().createForeignKey(currentModel, changedTable, change.getNewForeignKey()); change.apply(currentModel, isDelimitedIdentifierModeOn()); } /** * Processes a change representing the removal of a foreign key. * * @param currentModel The current database schema * @param params The parameters used in the creation of new tables. Note that for existing * tables, the parameters won't be applied * @param change The change object */ public void processChange(Database currentModel, CreationParameters params, RemoveForeignKeyChange change) throws IOException, ModelException { Table changedTable = findChangedTable(currentModel, change); ForeignKey changedFk = findChangedForeignKey(currentModel, change); getSqlBuilder().dropForeignKey(changedTable, changedFk); change.apply(currentModel, isDelimitedIdentifierModeOn()); } /** * Processes a change representing the addition of an index. * * @param currentModel The current database schema * @param params The parameters used in the creation of new tables. Note that for existing * tables, the parameters won't be applied * @param change The change object */ public void processChange(Database currentModel, CreationParameters params, AddIndexChange change) throws IOException { Table changedTable = findChangedTable(currentModel, change); getSqlBuilder().createIndex(changedTable, change.getNewIndex()); change.apply(currentModel, isDelimitedIdentifierModeOn()); } /** * Processes a change representing the removal of an index. * * @param currentModel The current database schema * @param params The parameters used in the creation of new tables. Note that for existing * tables, the parameters won't be applied * @param change The change object */ public void processChange(Database currentModel, CreationParameters params, RemoveIndexChange change) throws IOException, ModelException { Table changedTable = findChangedTable(currentModel, change); Index changedIndex = findChangedIndex(currentModel, change); getSqlBuilder().dropIndex(changedTable, changedIndex); change.apply(currentModel, isDelimitedIdentifierModeOn()); } /** * Processes a change representing the addition of a column. * * @param currentModel The current database schema * @param params The parameters used in the creation of new tables. Note that for existing * tables, the parameters won't be applied * @param change The change object */ public void processChange(Database currentModel, CreationParameters params, AddColumnChange change) throws IOException { Table changedTable = findChangedTable(currentModel, change); getSqlBuilder().addColumn(currentModel, changedTable, change.getNewColumn()); change.apply(currentModel, isDelimitedIdentifierModeOn()); } /** * Processes a change representing the addition of a primary key. * * @param currentModel The current database schema * @param params The parameters used in the creation of new tables. Note that for existing * tables, the parameters won't be applied * @param change The change object */ public void processChange(Database currentModel, CreationParameters params, AddPrimaryKeyChange change) throws IOException { Table changedTable = findChangedTable(currentModel, change); String[] pkColumnNames = change.getPrimaryKeyColumns(); Column[] pkColumns = new Column[pkColumnNames.length]; for (int colIdx = 0; colIdx < pkColumns.length; colIdx++) { pkColumns[colIdx] = changedTable.findColumn(pkColumnNames[colIdx], isDelimitedIdentifierModeOn()); } getSqlBuilder().createPrimaryKey(changedTable, pkColumns); change.apply(currentModel, isDelimitedIdentifierModeOn()); } /** * Processes a change representing the recreation of a table. * * @param currentModel The current database schema * @param params The parameters used in the creation of new tables. Note that for existing * tables, the parameters won't be applied * @param change The change object */ public void processChange(Database currentModel, CreationParameters params, RecreateTableChange change) throws IOException { // we can only copy the data if no required columns without default value and // non-autoincrement have been added boolean canMigrateData = true; for (Iterator it = change.getOriginalChanges().iterator(); canMigrateData && it.hasNext();) { TableChange curChange = (TableChange) it.next(); if (curChange instanceof AddColumnChange) { AddColumnChange addColumnChange = (AddColumnChange) curChange; if (addColumnChange.getNewColumn().isRequired() && !addColumnChange.getNewColumn().isAutoIncrement() && (addColumnChange.getNewColumn().getDefaultValue() == null)) { _log.warn("Data cannot be retained in table " + change.getChangedTable() + " because of the addition of the required column " + addColumnChange.getNewColumn().getName()); canMigrateData = false; } } } Table changedTable = findChangedTable(currentModel, change); Table targetTable = change.getTargetTable(); Map parameters = (params == null ? null : params.getParametersFor(targetTable)); if (canMigrateData) { Table tempTable = getTemporaryTableFor(targetTable); getSqlBuilder().createTemporaryTable(currentModel, tempTable, parameters); getSqlBuilder().copyData(changedTable, tempTable); // Note that we don't drop the indices here because the DROP TABLE will take care of that // Likewise, foreign keys have already been dropped as necessary getSqlBuilder().dropTable(changedTable); getSqlBuilder().createTable(currentModel, targetTable, parameters); getSqlBuilder().copyData(tempTable, targetTable); getSqlBuilder().dropTemporaryTable(currentModel, tempTable); } else { getSqlBuilder().dropTable(changedTable); getSqlBuilder().createTable(currentModel, targetTable, parameters); } change.apply(currentModel, isDelimitedIdentifierModeOn()); } /** * Creates a temporary table object that corresponds to the given table. * Database-specific implementations may redefine this method if e.g. the * database directly supports temporary tables. The default implementation * simply appends an underscore to the table name and uses that as the * table name. * * @param targetTable The target table * @return The temporary table */ protected Table getTemporaryTableFor(Table targetTable) { CloneHelper cloneHelper = new CloneHelper(); Table table = new Table(); table.setCatalog(targetTable.getCatalog()); table.setSchema(targetTable.getSchema()); table.setName(targetTable.getName() + "_"); table.setType(targetTable.getType()); for (int idx = 0; idx < targetTable.getColumnCount(); idx++) { // TODO: clone PK status ? table.addColumn(cloneHelper.clone(targetTable.getColumn(idx), true)); } return table; } /** * {@inheritDoc} */ public Iterator query(Database model, String sql) throws DatabaseOperationException { return query(model, sql, (Table[]) null); } /** * {@inheritDoc} */ public Iterator query(Database model, String sql, Collection parameters) throws DatabaseOperationException { return query(model, sql, parameters, null); } /** * {@inheritDoc} */ public Iterator query(Database model, String sql, Table[] queryHints) throws DatabaseOperationException { Connection connection = borrowConnection(); Statement statement = null; ResultSet resultSet = null; Iterator answer = null; try { statement = connection.createStatement(); resultSet = statement.executeQuery(sql); answer = createResultSetIterator(model, resultSet, queryHints); return answer; } catch (SQLException ex) { throw new DatabaseOperationException("Error while performing a query", ex); } finally { // if any exceptions are thrown, close things down // otherwise we're leaving it open for the iterator if (answer == null) { closeStatement(statement); returnConnection(connection); } } } /** * {@inheritDoc} */ public Iterator query(Database model, String sql, Collection parameters, Table[] queryHints) throws DatabaseOperationException { Connection connection = borrowConnection(); PreparedStatement statement = null; ResultSet resultSet = null; Iterator answer = null; try { statement = connection.prepareStatement(sql); int paramIdx = 1; for (Iterator iter = parameters.iterator(); iter.hasNext(); paramIdx++) { Object arg = iter.next(); if (arg instanceof BigDecimal) { // to avoid scale problems because setObject assumes a scale of 0 statement.setBigDecimal(paramIdx, (BigDecimal) arg); } else { statement.setObject(paramIdx, arg); } } resultSet = statement.executeQuery(); answer = createResultSetIterator(model, resultSet, queryHints); return answer; } catch (SQLException ex) { throw new DatabaseOperationException("Error while performing a query", ex); } finally { // if any exceptions are thrown, close things down // otherwise we're leaving it open for the iterator if (answer == null) { closeStatement(statement); returnConnection(connection); } } } /** * {@inheritDoc} */ public List fetch(Database model, String sql) throws DatabaseOperationException { return fetch(model, sql, (Table[]) null, 0, -1); } /** * {@inheritDoc} */ public List fetch(Database model, String sql, Table[] queryHints) throws DatabaseOperationException { return fetch(model, sql, queryHints, 0, -1); } /** * {@inheritDoc} */ public List fetch(Database model, String sql, int start, int end) throws DatabaseOperationException { return fetch(model, sql, (Table[]) null, start, end); } /** * {@inheritDoc} */ public List fetch(Database model, String sql, Table[] queryHints, int start, int end) throws DatabaseOperationException { Connection connection = borrowConnection(); Statement statement = null; ResultSet resultSet = null; List result = new ArrayList(); try { statement = connection.createStatement(); resultSet = statement.executeQuery(sql); int rowIdx = 0; for (ModelBasedResultSetIterator it = createResultSetIterator(model, resultSet, queryHints); ((end < 0) || (rowIdx <= end)) && it.hasNext(); rowIdx++) { if (rowIdx >= start) { result.add(it.next()); } else { it.advance(); } } } catch (SQLException ex) { throw new DatabaseOperationException("Error while fetching data from the database", ex); } finally { // the iterator should return the connection automatically // so this is usually not necessary (but just in case) closeStatement(statement); returnConnection(connection); } return result; } /** * {@inheritDoc} */ public List fetch(Database model, String sql, Collection parameters) throws DatabaseOperationException { return fetch(model, sql, parameters, null, 0, -1); } /** * {@inheritDoc} */ public List fetch(Database model, String sql, Collection parameters, int start, int end) throws DatabaseOperationException { return fetch(model, sql, parameters, null, start, end); } /** * {@inheritDoc} */ public List fetch(Database model, String sql, Collection parameters, Table[] queryHints) throws DatabaseOperationException { return fetch(model, sql, parameters, queryHints, 0, -1); } /** * {@inheritDoc} */ public List fetch(Database model, String sql, Collection parameters, Table[] queryHints, int start, int end) throws DatabaseOperationException { Connection connection = borrowConnection(); PreparedStatement statement = null; ResultSet resultSet = null; List result = new ArrayList(); try { statement = connection.prepareStatement(sql); int paramIdx = 1; for (Iterator iter = parameters.iterator(); iter.hasNext(); paramIdx++) { Object arg = iter.next(); if (arg instanceof BigDecimal) { // to avoid scale problems because setObject assumes a scale of 0 statement.setBigDecimal(paramIdx, (BigDecimal) arg); } else { statement.setObject(paramIdx, arg); } } resultSet = statement.executeQuery(); int rowIdx = 0; for (ModelBasedResultSetIterator it = createResultSetIterator(model, resultSet, queryHints); ((end < 0) || (rowIdx <= end)) && it.hasNext(); rowIdx++) { if (rowIdx >= start) { result.add(it.next()); } else { it.advance(); } } } catch (SQLException ex) { // any other exception comes from the iterator which closes the resources automatically closeStatement(statement); returnConnection(connection); throw new DatabaseOperationException("Error while fetching data from the database", ex); } return result; } /** * Creates the SQL for inserting an object of the given type. If a concrete bean is given, * then a concrete insert statement is created, otherwise an insert statement usable in a * prepared statement is build. * * @param model The database model * @param dynaClass The type * @param properties The properties to write * @param bean Optionally the concrete bean to insert * @return The SQL required to insert an instance of the class */ protected String createInsertSql(Database model, SqlDynaClass dynaClass, SqlDynaProperty[] properties, DynaBean bean) { Table table = model.findTable(dynaClass.getTableName()); HashMap columnValues = toColumnValues(properties, bean); return _builder.getInsertSql(table, columnValues, bean == null); } /** * Creates the SQL for querying for the id generated by the last insert of an object of the given type. * * @param model The database model * @param dynaClass The type * @return The SQL required for querying for the id, or <code>null</code> if the database does not * support this */ protected String createSelectLastInsertIdSql(Database model, SqlDynaClass dynaClass) { Table table = model.findTable(dynaClass.getTableName()); return _builder.getSelectLastIdentityValues(table); } /** * {@inheritDoc} */ public String getInsertSql(Database model, DynaBean dynaBean) { SqlDynaClass dynaClass = model.getDynaClassFor(dynaBean); SqlDynaProperty[] properties = dynaClass.getSqlDynaProperties(); if (properties.length == 0) { _log.info("Cannot insert instances of type " + dynaClass + " because it has no properties"); return null; } return createInsertSql(model, dynaClass, properties, dynaBean); } /** * Returns all properties where the column is not non-autoincrement and for which the bean * either has a value or the column hasn't got a default value, for the given dyna class. * * @param model The database model * @param dynaClass The dyna class * @param bean The bean * @return The properties */ private SqlDynaProperty[] getPropertiesForInsertion(Database model, SqlDynaClass dynaClass, final DynaBean bean) { SqlDynaProperty[] properties = dynaClass.getSqlDynaProperties(); Collection result = CollectionUtils.select(Arrays.asList(properties), new Predicate() { public boolean evaluate(Object input) { SqlDynaProperty prop = (SqlDynaProperty) input; if (bean.get(prop.getName()) != null) { // we ignore properties for which a value is present in the bean // only if they are identity and identity override is off or // the platform does not allow the override of the auto-increment // specification return !prop.getColumn().isAutoIncrement() || (isIdentityOverrideOn() && getPlatformInfo().isIdentityOverrideAllowed()); } else { // we also return properties without a value in the bean // if they ain't auto-increment and don't have a default value // in this case, a NULL is inserted return !prop.getColumn().isAutoIncrement() && (prop.getColumn().getDefaultValue() == null); } } }); return (SqlDynaProperty[]) result.toArray(new SqlDynaProperty[result.size()]); } /** * Returns all identity properties whose value were defined by the database and which * now need to be read back from the DB. * * @param model The database model * @param dynaClass The dyna class * @param bean The bean * @return The columns */ private Column[] getRelevantIdentityColumns(Database model, SqlDynaClass dynaClass, final DynaBean bean) { SqlDynaProperty[] properties = dynaClass.getSqlDynaProperties(); Collection relevantProperties = CollectionUtils.select(Arrays.asList(properties), new Predicate() { public boolean evaluate(Object input) { SqlDynaProperty prop = (SqlDynaProperty) input; // we only want those identity columns that were really specified by the DB // if the platform allows specification of values for identity columns // in INSERT/UPDATE statements, then we need to filter the corresponding // columns out return prop.getColumn().isAutoIncrement() && (!isIdentityOverrideOn() || !getPlatformInfo().isIdentityOverrideAllowed() || (bean.get(prop.getName()) == null)); } }); Column[] columns = new Column[relevantProperties.size()]; int idx = 0; for (Iterator propIt = relevantProperties.iterator(); propIt.hasNext(); idx++) { columns[idx] = ((SqlDynaProperty) propIt.next()).getColumn(); } return columns; } /** * {@inheritDoc} */ public void insert(Connection connection, Database model, DynaBean dynaBean) throws DatabaseOperationException { SqlDynaClass dynaClass = model.getDynaClassFor(dynaBean); SqlDynaProperty[] properties = getPropertiesForInsertion(model, dynaClass, dynaBean); Column[] autoIncrColumns = getRelevantIdentityColumns(model, dynaClass, dynaBean); if ((properties.length == 0) && (autoIncrColumns.length == 0)) { _log.warn("Cannot insert instances of type " + dynaClass + " because it has no usable properties"); return; } String insertSql = createInsertSql(model, dynaClass, properties, null); String queryIdentitySql = null; if (_log.isDebugEnabled()) { _log.debug("About to execute SQL: " + insertSql); } if (autoIncrColumns.length > 0) { if (!getPlatformInfo().isLastIdentityValueReadable()) { _log.warn("The database does not support querying for auto-generated column values"); } else { queryIdentitySql = createSelectLastInsertIdSql(model, dynaClass); } } boolean autoCommitMode = false; PreparedStatement statement = null; try { if (!getPlatformInfo().isAutoCommitModeForLastIdentityValueReading()) { autoCommitMode = connection.getAutoCommit(); connection.setAutoCommit(false); } beforeInsert(connection, dynaClass.getTable()); statement = connection.prepareStatement(insertSql); for (int idx = 0; idx < properties.length; idx++) { setObject(statement, idx + 1, dynaBean, properties[idx]); } int count = statement.executeUpdate(); afterInsert(connection, dynaClass.getTable()); if (count != 1) { _log.warn("Attempted to insert a single row " + dynaBean + " in table " + dynaClass.getTableName() + " but changed " + count + " row(s)"); } } catch (SQLException ex) { throw new DatabaseOperationException("Error while inserting into the database: " + ex.getMessage(), ex); } finally { closeStatement(statement); } if (queryIdentitySql != null) { Statement queryStmt = null; ResultSet lastInsertedIds = null; try { if (getPlatformInfo().isAutoCommitModeForLastIdentityValueReading()) { // we'll commit the statement(s) if no auto-commit is enabled because // otherwise it is possible that the auto increment hasn't happened yet // (the db didn't actually perform the insert yet so no triggering of // sequences did occur) if (!connection.getAutoCommit()) { connection.commit(); } } queryStmt = connection.createStatement(); lastInsertedIds = queryStmt.executeQuery(queryIdentitySql); lastInsertedIds.next(); for (int idx = 0; idx < autoIncrColumns.length; idx++) { // we're using the index rather than the name because we cannot know how // the SQL statement looks like; rather we assume that we get the values // back in the same order as the auto increment columns Object value = getObjectFromResultSet(lastInsertedIds, autoIncrColumns[idx], idx + 1); PropertyUtils.setProperty(dynaBean, autoIncrColumns[idx].getName(), value); } } catch (NoSuchMethodException ex) { // Can't happen because we're using dyna beans } catch (IllegalAccessException ex) { // Can't happen because we're using dyna beans } catch (InvocationTargetException ex) { // Can't happen because we're using dyna beans } catch (SQLException ex) { throw new DatabaseOperationException( "Error while retrieving the identity column value(s) from the database", ex); } finally { if (lastInsertedIds != null) { try { lastInsertedIds.close(); } catch (SQLException ex) { // we ignore this one } } closeStatement(statement); } } if (!getPlatformInfo().isAutoCommitModeForLastIdentityValueReading()) { try { // we need to do a manual commit now connection.commit(); connection.setAutoCommit(autoCommitMode); } catch (SQLException ex) { throw new DatabaseOperationException(ex); } } } /** * {@inheritDoc} */ public void insert(Database model, DynaBean dynaBean) throws DatabaseOperationException { Connection connection = borrowConnection(); try { insert(connection, model, dynaBean); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public void insert(Connection connection, Database model, Collection dynaBeans) throws DatabaseOperationException { SqlDynaClass dynaClass = null; SqlDynaProperty[] properties = null; PreparedStatement statement = null; int addedStmts = 0; boolean identityWarningPrinted = false; for (Iterator it = dynaBeans.iterator(); it.hasNext();) { DynaBean dynaBean = (DynaBean) it.next(); SqlDynaClass curDynaClass = model.getDynaClassFor(dynaBean); if (curDynaClass != dynaClass) { if (dynaClass != null) { executeBatch(statement, addedStmts, dynaClass.getTable()); addedStmts = 0; } dynaClass = curDynaClass; properties = getPropertiesForInsertion(model, curDynaClass, dynaBean); if (properties.length == 0) { _log.warn("Cannot insert instances of type " + dynaClass + " because it has no usable properties"); continue; } if (!identityWarningPrinted && (getRelevantIdentityColumns(model, curDynaClass, dynaBean).length > 0)) { _log.warn( "Updating the bean properties corresponding to auto-increment columns is not supported in batch mode"); identityWarningPrinted = true; } String insertSql = createInsertSql(model, dynaClass, properties, null); if (_log.isDebugEnabled()) { _log.debug("Starting new batch with SQL: " + insertSql); } try { statement = connection.prepareStatement(insertSql); } catch (SQLException ex) { throw new DatabaseOperationException("Error while preparing insert statement", ex); } } try { for (int idx = 0; idx < properties.length; idx++) { setObject(statement, idx + 1, dynaBean, properties[idx]); } statement.addBatch(); addedStmts++; } catch (SQLException ex) { throw new DatabaseOperationException("Error while adding batch insert", ex); } } if (dynaClass != null) { executeBatch(statement, addedStmts, dynaClass.getTable()); } } /** * Performs the batch for the given statement, and checks that the specified amount of rows have been changed. * * @param statement The prepared statement * @param numRows The number of rows that should change * @param table The changed table */ private void executeBatch(PreparedStatement statement, int numRows, Table table) throws DatabaseOperationException { if (statement != null) { try { Connection connection = statement.getConnection(); beforeInsert(connection, table); int[] results = statement.executeBatch(); closeStatement(statement); afterInsert(connection, table); boolean hasSum = true; int sum = 0; for (int idx = 0; (results != null) && (idx < results.length); idx++) { if (results[idx] < 0) { hasSum = false; if (results[idx] == Statement.EXECUTE_FAILED) { _log.warn("The batch insertion of row " + idx + " into table " + table.getName() + " failed but the driver is able to continue processing"); } else if (results[idx] != Statement.SUCCESS_NO_INFO) { _log.warn("The batch insertion of row " + idx + " into table " + table.getName() + " returned an undefined status value " + results[idx]); } } else { sum += results[idx]; } } if (hasSum && (sum != numRows)) { _log.warn("Attempted to insert " + numRows + " rows into table " + table.getName() + " but changed " + sum + " rows"); } } catch (SQLException ex) { if (ex instanceof BatchUpdateException) { SQLException sqlEx = ((BatchUpdateException) ex).getNextException(); throw new DatabaseOperationException("Error while inserting into the database", sqlEx); } else { throw new DatabaseOperationException("Error while inserting into the database", ex); } } } } /** * {@inheritDoc} */ public void insert(Database model, Collection dynaBeans) throws DatabaseOperationException { Connection connection = borrowConnection(); try { insert(connection, model, dynaBeans); } finally { returnConnection(connection); } } /** * Allows platforms to issue statements directly before rows are inserted into * the specified table. * * @param connection The connection used for the insertion * @param table The table that the rows are inserted into */ protected void beforeInsert(Connection connection, Table table) throws SQLException { } /** * Allows platforms to issue statements directly after rows have been inserted into * the specified table. * * @param connection The connection used for the insertion * @param table The table that the rows have been inserted into */ protected void afterInsert(Connection connection, Table table) throws SQLException { } /** * Creates the SQL for updating an object of the given type. If a concrete bean is given, * then a concrete update statement is created, otherwise an update statement usable in a * prepared statement is build. * * @param model The database model * @param dynaClass The type * @param primaryKeys The primary keys * @param properties The properties to write * @param bean Optionally the concrete bean to update * @return The SQL required to update the instance */ protected String createUpdateSql(Database model, SqlDynaClass dynaClass, SqlDynaProperty[] primaryKeys, SqlDynaProperty[] properties, DynaBean bean) { Table table = model.findTable(dynaClass.getTableName()); HashMap columnValues = toColumnValues(properties, bean); columnValues.putAll(toColumnValues(primaryKeys, bean)); return _builder.getUpdateSql(table, columnValues, bean == null); } /** * Creates the SQL for updating an object of the given type. If a concrete bean is given, * then a concrete update statement is created, otherwise an update statement usable in a * prepared statement is build. * * @param model The database model * @param dynaClass The type * @param primaryKeys The primary keys * @param properties The properties to write * @param oldBean Contains column values to identify the rows to update (i.e. for the WHERE clause) * @param newBean Contains the new column values to write * @return The SQL required to update the instance */ protected String createUpdateSql(Database model, SqlDynaClass dynaClass, SqlDynaProperty[] primaryKeys, SqlDynaProperty[] properties, DynaBean oldBean, DynaBean newBean) { Table table = model.findTable(dynaClass.getTableName()); HashMap oldColumnValues = toColumnValues(primaryKeys, oldBean); HashMap newColumnValues = toColumnValues(properties, newBean); if (primaryKeys.length == 0) { _log.info("Cannot update instances of type " + dynaClass + " because it has no primary keys"); return null; } else { return _builder.getUpdateSql(table, oldColumnValues, newColumnValues, newBean == null); } } /** * {@inheritDoc} */ public String getUpdateSql(Database model, DynaBean dynaBean) { SqlDynaClass dynaClass = model.getDynaClassFor(dynaBean); SqlDynaProperty[] primaryKeys = dynaClass.getPrimaryKeyProperties(); SqlDynaProperty[] nonPrimaryKeys = dynaClass.getNonPrimaryKeyProperties(); if (primaryKeys.length == 0) { _log.info("Cannot update instances of type " + dynaClass + " because it has no primary keys"); return null; } else { return createUpdateSql(model, dynaClass, primaryKeys, nonPrimaryKeys, dynaBean); } } /** * {@inheritDoc} */ public String getUpdateSql(Database model, DynaBean oldDynaBean, DynaBean newDynaBean) { SqlDynaClass dynaClass = model.getDynaClassFor(oldDynaBean); SqlDynaProperty[] primaryKeys = dynaClass.getPrimaryKeyProperties(); SqlDynaProperty[] nonPrimaryKeys = dynaClass.getNonPrimaryKeyProperties(); if (primaryKeys.length == 0) { _log.info("Cannot update instances of type " + dynaClass + " because it has no primary keys"); return null; } else { return createUpdateSql(model, dynaClass, primaryKeys, nonPrimaryKeys, oldDynaBean, newDynaBean); } } /** * {@inheritDoc} */ public void update(Connection connection, Database model, DynaBean dynaBean) throws DatabaseOperationException { SqlDynaClass dynaClass = model.getDynaClassFor(dynaBean); SqlDynaProperty[] primaryKeys = dynaClass.getPrimaryKeyProperties(); if (primaryKeys.length == 0) { _log.info("Cannot update instances of type " + dynaClass + " because it has no primary keys"); return; } SqlDynaProperty[] properties = dynaClass.getNonPrimaryKeyProperties(); String sql = createUpdateSql(model, dynaClass, primaryKeys, properties, null); PreparedStatement statement = null; if (_log.isDebugEnabled()) { _log.debug("About to execute SQL: " + sql); } try { beforeUpdate(connection, dynaClass.getTable()); statement = connection.prepareStatement(sql); int sqlIndex = 1; for (int idx = 0; idx < properties.length; idx++) { setObject(statement, sqlIndex++, dynaBean, properties[idx]); } for (int idx = 0; idx < primaryKeys.length; idx++) { setObject(statement, sqlIndex++, dynaBean, primaryKeys[idx]); } int count = statement.executeUpdate(); afterUpdate(connection, dynaClass.getTable()); if (count != 1) { _log.warn("Attempted to insert a single row " + dynaBean + " into table " + dynaClass.getTableName() + " but changed " + count + " row(s)"); } } catch (SQLException ex) { throw new DatabaseOperationException("Error while updating in the database", ex); } finally { closeStatement(statement); } } /** * {@inheritDoc} */ public void update(Database model, DynaBean dynaBean) throws DatabaseOperationException { Connection connection = borrowConnection(); try { update(connection, model, dynaBean); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public void update(Connection connection, Database model, DynaBean oldDynaBean, DynaBean newDynaBean) throws DatabaseOperationException { SqlDynaClass dynaClass = model.getDynaClassFor(oldDynaBean); SqlDynaProperty[] primaryKeys = dynaClass.getPrimaryKeyProperties(); if (!dynaClass.getTable().equals(model.getDynaClassFor(newDynaBean).getTable())) { throw new DatabaseOperationException("The old and new dyna beans need to be for the same table"); } if (primaryKeys.length == 0) { _log.info("Cannot update instances of type " + dynaClass + " because it has no primary keys"); return; } SqlDynaProperty[] properties = dynaClass.getSqlDynaProperties(); String sql = createUpdateSql(model, dynaClass, primaryKeys, properties, null, null); PreparedStatement statement = null; if (_log.isDebugEnabled()) { _log.debug("About to execute SQL: " + sql); } try { beforeUpdate(connection, dynaClass.getTable()); statement = connection.prepareStatement(sql); int sqlIndex = 1; for (int idx = 0; idx < properties.length; idx++) { setObject(statement, sqlIndex++, newDynaBean, properties[idx]); } for (int idx = 0; idx < primaryKeys.length; idx++) { setObject(statement, sqlIndex++, oldDynaBean, primaryKeys[idx]); } int count = statement.executeUpdate(); afterUpdate(connection, dynaClass.getTable()); if (count != 1) { _log.warn("Attempted to insert a single row " + newDynaBean + " into table " + dynaClass.getTableName() + " but changed " + count + " row(s)"); } } catch (SQLException ex) { throw new DatabaseOperationException("Error while updating in the database", ex); } finally { closeStatement(statement); } } /** * {@inheritDoc} */ public void update(Database model, DynaBean oldDynaBean, DynaBean newDynaBean) throws DatabaseOperationException { Connection connection = borrowConnection(); try { update(connection, model, oldDynaBean, newDynaBean); } finally { returnConnection(connection); } } /** * Allows platforms to issue statements directly before rows are updated in * the specified table. * * @param connection The connection used for the update * @param table The table that the rows are updateed into */ protected void beforeUpdate(Connection connection, Table table) throws SQLException { } /** * Allows platforms to issue statements directly after rows have been updated in * the specified table. * * @param connection The connection used for the update * @param table The table that the rows have been updateed into */ protected void afterUpdate(Connection connection, Table table) throws SQLException { } /** * {@inheritDoc} */ public boolean exists(Database model, DynaBean dynaBean) { Connection connection = borrowConnection(); try { return exists(connection, model, dynaBean); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public boolean exists(Connection connection, Database model, DynaBean dynaBean) { SqlDynaClass dynaClass = model.getDynaClassFor(dynaBean); SqlDynaProperty[] primaryKeys = dynaClass.getPrimaryKeyProperties(); if (primaryKeys.length == 0) { return false; } PreparedStatement stmt = null; try { StringBuffer sql = new StringBuffer(); sql.append("SELECT * FROM "); sql.append(_builder.getDelimitedIdentifier(dynaClass.getTable().getName())); sql.append(" WHERE "); for (int idx = 0; idx < primaryKeys.length; idx++) { String key = primaryKeys[idx].getColumn().getName(); if (idx > 0) { sql.append(" AND "); } sql.append(_builder.getDelimitedIdentifier(key)); sql.append("=?"); } stmt = connection.prepareStatement(sql.toString()); for (int idx = 0; idx < primaryKeys.length; idx++) { setObject(stmt, idx + 1, dynaBean, primaryKeys[idx]); } ResultSet resultSet = stmt.executeQuery(); return resultSet.next(); } catch (SQLException ex) { throw new DatabaseOperationException("Error while reading from the database", ex); } finally { closeStatement(stmt); } } /** * {@inheritDoc} */ public void store(Database model, DynaBean dynaBean) throws DatabaseOperationException { Connection connection = borrowConnection(); try { store(connection, model, dynaBean); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public void store(Connection connection, Database model, DynaBean dynaBean) throws DatabaseOperationException { if (exists(connection, model, dynaBean)) { update(connection, model, dynaBean); } else { insert(connection, model, dynaBean); } } /** * Creates the SQL for deleting an object of the given type. If a concrete bean is given, * then a concrete delete statement is created, otherwise a delete statement usable in a * prepared statement is build. * * @param model The database model * @param dynaClass The type * @param primaryKeys The primary keys * @param bean Optionally the concrete bean to update * @return The SQL required to delete the instance */ protected String createDeleteSql(Database model, SqlDynaClass dynaClass, SqlDynaProperty[] primaryKeys, DynaBean bean) { Table table = model.findTable(dynaClass.getTableName()); HashMap pkValues = toColumnValues(primaryKeys, bean); return _builder.getDeleteSql(table, pkValues, bean == null); } /** * {@inheritDoc} */ public String getDeleteSql(Database model, DynaBean dynaBean) { SqlDynaClass dynaClass = model.getDynaClassFor(dynaBean); SqlDynaProperty[] primaryKeys = dynaClass.getPrimaryKeyProperties(); if (primaryKeys.length == 0) { _log.warn("Cannot delete instances of type " + dynaClass + " because it has no primary keys"); return null; } else { return createDeleteSql(model, dynaClass, primaryKeys, dynaBean); } } /** * {@inheritDoc} */ public void delete(Database model, DynaBean dynaBean) throws DatabaseOperationException { Connection connection = borrowConnection(); try { delete(connection, model, dynaBean); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public void delete(Connection connection, Database model, DynaBean dynaBean) throws DatabaseOperationException { PreparedStatement statement = null; try { SqlDynaClass dynaClass = model.getDynaClassFor(dynaBean); SqlDynaProperty[] primaryKeys = dynaClass.getPrimaryKeyProperties(); if (primaryKeys.length == 0) { _log.warn("Cannot delete instances of type " + dynaClass + " because it has no primary keys"); return; } String sql = createDeleteSql(model, dynaClass, primaryKeys, null); if (_log.isDebugEnabled()) { _log.debug("About to execute SQL " + sql); } statement = connection.prepareStatement(sql); for (int idx = 0; idx < primaryKeys.length; idx++) { setObject(statement, idx + 1, dynaBean, primaryKeys[idx]); } int count = statement.executeUpdate(); if (count != 1) { _log.warn("Attempted to delete a single row " + dynaBean + " in table " + dynaClass.getTableName() + " but changed " + count + " row(s)."); } } catch (SQLException ex) { throw new DatabaseOperationException("Error while deleting from the database", ex); } finally { closeStatement(statement); } } /** * {@inheritDoc} */ public Database readModelFromDatabase(String name) throws DatabaseOperationException { Connection connection = borrowConnection(); try { return readModelFromDatabase(connection, name); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public Database readModelFromDatabase(Connection connection, String name) throws DatabaseOperationException { try { Database model = getModelReader().getDatabase(connection, name); postprocessModelFromDatabase(model); return model; } catch (SQLException ex) { throw new DatabaseOperationException(ex); } } /** * {@inheritDoc} */ public Database readModelFromDatabase(String name, String catalog, String schema, String[] tableTypes) throws DatabaseOperationException { Connection connection = borrowConnection(); try { return readModelFromDatabase(connection, name, catalog, schema, tableTypes); } finally { returnConnection(connection); } } /** * {@inheritDoc} */ public Database readModelFromDatabase(Connection connection, String name, String catalog, String schema, String[] tableTypes) throws DatabaseOperationException { try { JdbcModelReader reader = getModelReader(); Database model = reader.getDatabase(connection, name, catalog, schema, tableTypes); postprocessModelFromDatabase(model); if ((model.getName() == null) || (model.getName().length() == 0)) { model.setName(MODEL_DEFAULT_NAME); } return model; } catch (SQLException ex) { throw new DatabaseOperationException(ex); } } /** * Allows the platform to postprocess the model just read from the database. * * @param model The model */ protected void postprocessModelFromDatabase(Database model) { // Default values for CHAR/VARCHAR/LONGVARCHAR columns have quotation marks // around them which we'll remove now for (int tableIdx = 0; tableIdx < model.getTableCount(); tableIdx++) { Table table = model.getTable(tableIdx); for (int columnIdx = 0; columnIdx < table.getColumnCount(); columnIdx++) { Column column = table.getColumn(columnIdx); if (TypeMap.isTextType(column.getTypeCode()) || TypeMap.isDateTimeType(column.getTypeCode())) { String defaultValue = column.getDefaultValue(); if ((defaultValue != null) && (defaultValue.length() >= 2) && defaultValue.startsWith("'") && defaultValue.endsWith("'")) { defaultValue = defaultValue.substring(1, defaultValue.length() - 1); column.setDefaultValue(defaultValue); } } } } } /** * Derives the column values for the given dyna properties from the dyna bean. * * @param properties The properties * @param bean The bean * @return The values indexed by the column names */ protected HashMap toColumnValues(SqlDynaProperty[] properties, DynaBean bean) { HashMap result = new HashMap(); for (int idx = 0; idx < properties.length; idx++) { result.put(properties[idx].getName(), bean == null ? null : bean.get(properties[idx].getName())); } return result; } /** * Sets a parameter of the prepared statement based on the type of the column of the property. * * @param statement The statement * @param sqlIndex The index of the parameter to set in the statement * @param dynaBean The bean of which to take the value * @param property The property of the bean, which also defines the corresponding column */ protected void setObject(PreparedStatement statement, int sqlIndex, DynaBean dynaBean, SqlDynaProperty property) throws SQLException { int typeCode = property.getColumn().getTypeCode(); Object value = dynaBean.get(property.getName()); setStatementParameterValue(statement, sqlIndex, typeCode, value); } /** * This is the core method to set the parameter of a prepared statement to a given value. * The primary purpose of this method is to call the appropriate method on the statement, * and to give database-specific implementations the ability to change this behavior. * * @param statement The statement * @param sqlIndex The parameter index * @param typeCode The JDBC type code * @param value The value * @throws SQLException If an error occurred while setting the parameter value */ protected void setStatementParameterValue(PreparedStatement statement, int sqlIndex, int typeCode, Object value) throws SQLException { if (value == null) { statement.setNull(sqlIndex, typeCode); } else if (value instanceof String) { statement.setString(sqlIndex, (String) value); } else if (value instanceof byte[]) { statement.setBytes(sqlIndex, (byte[]) value); } else if (value instanceof Boolean) { statement.setBoolean(sqlIndex, ((Boolean) value).booleanValue()); } else if (value instanceof Byte) { statement.setByte(sqlIndex, ((Byte) value).byteValue()); } else if (value instanceof Short) { statement.setShort(sqlIndex, ((Short) value).shortValue()); } else if (value instanceof Integer) { statement.setInt(sqlIndex, ((Integer) value).intValue()); } else if (value instanceof Long) { statement.setLong(sqlIndex, ((Long) value).longValue()); } else if (value instanceof BigDecimal) { // setObject assumes a scale of 0, so we rather use the typed setter statement.setBigDecimal(sqlIndex, (BigDecimal) value); } else if (value instanceof Float) { statement.setFloat(sqlIndex, ((Float) value).floatValue()); } else if (value instanceof Double) { statement.setDouble(sqlIndex, ((Double) value).doubleValue()); } else { statement.setObject(sqlIndex, value, typeCode); } } /** * Helper method esp. for the {@link ModelBasedResultSetIterator} class that retrieves * the value for a column from the given result set. If a table was specified, * and it contains the column, then the jdbc type defined for the column is used for extracting * the value, otherwise the object directly retrieved from the result set is returned.<br/> * The method is defined here rather than in the {@link ModelBasedResultSetIterator} class * so that concrete platforms can modify its behavior. * * @param resultSet The result set * @param columnName The name of the column * @param table The table * @return The value */ protected Object getObjectFromResultSet(ResultSet resultSet, String columnName, Table table) throws SQLException { Column column = (table == null ? null : table.findColumn(columnName, isDelimitedIdentifierModeOn())); Object value = null; if (column != null) { int originalJdbcType = column.getTypeCode(); int targetJdbcType = getPlatformInfo().getTargetJdbcType(originalJdbcType); int jdbcType = originalJdbcType; // in general we're trying to retrieve the value using the original type // but sometimes we also need the target type: if ((originalJdbcType == Types.BLOB) && (targetJdbcType != Types.BLOB)) { // we should not use the Blob interface if the database doesn't map to this type jdbcType = targetJdbcType; } if ((originalJdbcType == Types.CLOB) && (targetJdbcType != Types.CLOB)) { // we should not use the Clob interface if the database doesn't map to this type jdbcType = targetJdbcType; } value = extractColumnValue(resultSet, columnName, 0, jdbcType); } else { value = resultSet.getObject(columnName); } return resultSet.wasNull() ? null : value; } /** * Helper method for retrieving the value for a column from the given result set * using the type code of the column. * * @param resultSet The result set * @param column The column * @param idx The value's index in the result set (starting from 1) * @return The value */ protected Object getObjectFromResultSet(ResultSet resultSet, Column column, int idx) throws SQLException { int originalJdbcType = column.getTypeCode(); int targetJdbcType = getPlatformInfo().getTargetJdbcType(originalJdbcType); int jdbcType = originalJdbcType; Object value = null; // in general we're trying to retrieve the value using the original type // but sometimes we also need the target type: if ((originalJdbcType == Types.BLOB) && (targetJdbcType != Types.BLOB)) { // we should not use the Blob interface if the database doesn't map to this type jdbcType = targetJdbcType; } if ((originalJdbcType == Types.CLOB) && (targetJdbcType != Types.CLOB)) { // we should not use the Clob interface if the database doesn't map to this type jdbcType = targetJdbcType; } value = extractColumnValue(resultSet, null, idx, jdbcType); return resultSet.wasNull() ? null : value; } /** * This is the core method to retrieve a value for a column from a result set. Its primary * purpose is to call the appropriate method on the result set, and to provide an extension * point where database-specific implementations can change this behavior. * * @param resultSet The result set to extract the value from * @param columnName The name of the column; can be <code>null</code> in which case the * <code>columnIdx</code> will be used instead * @param columnIdx The index of the column's value in the result set; is only used if * <code>columnName</code> is <code>null</code> * @param jdbcType The jdbc type to extract * @return The value * @throws SQLException If an error occurred while accessing the result set */ protected Object extractColumnValue(ResultSet resultSet, String columnName, int columnIdx, int jdbcType) throws SQLException { boolean useIdx = (columnName == null); Object value; switch (jdbcType) { case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: value = useIdx ? resultSet.getString(columnIdx) : resultSet.getString(columnName); break; case Types.NUMERIC: case Types.DECIMAL: value = useIdx ? resultSet.getBigDecimal(columnIdx) : resultSet.getBigDecimal(columnName); break; case Types.BIT: case Types.BOOLEAN: value = new Boolean(useIdx ? resultSet.getBoolean(columnIdx) : resultSet.getBoolean(columnName)); break; case Types.TINYINT: case Types.SMALLINT: case Types.INTEGER: value = new Integer(useIdx ? resultSet.getInt(columnIdx) : resultSet.getInt(columnName)); break; case Types.BIGINT: value = new Long(useIdx ? resultSet.getLong(columnIdx) : resultSet.getLong(columnName)); break; case Types.REAL: value = new Float(useIdx ? resultSet.getFloat(columnIdx) : resultSet.getFloat(columnName)); break; case Types.FLOAT: case Types.DOUBLE: value = new Double(useIdx ? resultSet.getDouble(columnIdx) : resultSet.getDouble(columnName)); break; case Types.BINARY: case Types.VARBINARY: case Types.LONGVARBINARY: value = useIdx ? resultSet.getBytes(columnIdx) : resultSet.getBytes(columnName); break; case Types.DATE: value = useIdx ? resultSet.getDate(columnIdx) : resultSet.getDate(columnName); break; case Types.TIME: value = useIdx ? resultSet.getTime(columnIdx) : resultSet.getTime(columnName); break; case Types.TIMESTAMP: value = useIdx ? resultSet.getTimestamp(columnIdx) : resultSet.getTimestamp(columnName); break; case Types.CLOB: Clob clob = useIdx ? resultSet.getClob(columnIdx) : resultSet.getClob(columnName); if (clob == null) { value = null; } else { long length = clob.length(); if (length > Integer.MAX_VALUE) { value = clob; } else if (length == 0) { // the javadoc is not clear about whether Clob.getSubString // can be used with a substring length of 0 // thus we do the safe thing and handle it ourselves value = ""; } else { value = clob.getSubString(1l, (int) length); } } break; case Types.BLOB: Blob blob = useIdx ? resultSet.getBlob(columnIdx) : resultSet.getBlob(columnName); if (blob == null) { value = null; } else { long length = blob.length(); if (length > Integer.MAX_VALUE) { value = blob; } else if (length == 0) { // the javadoc is not clear about whether Blob.getBytes // can be used with for 0 bytes to be copied // thus we do the safe thing and handle it ourselves value = new byte[0]; } else { value = blob.getBytes(1l, (int) length); } } break; case Types.ARRAY: value = useIdx ? resultSet.getArray(columnIdx) : resultSet.getArray(columnName); break; case Types.REF: value = useIdx ? resultSet.getRef(columnIdx) : resultSet.getRef(columnName); break; default: value = useIdx ? resultSet.getObject(columnIdx) : resultSet.getObject(columnName); break; } return resultSet.wasNull() ? null : value; } /** * Creates an iterator over the given result set. * * @param model The database model * @param resultSet The result set to iterate over * @param queryHints The tables that were queried in the query that produced the * given result set (optional) * @return The iterator */ protected ModelBasedResultSetIterator createResultSetIterator(Database model, ResultSet resultSet, Table[] queryHints) { return new ModelBasedResultSetIterator(this, model, resultSet, queryHints, true); } }