Java tutorial
/* * Copyright (c) 2008-2011 Simon Ritchie. * All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see http://www.gnu.org/licenses/>. */ package org.rimudb; import java.sql.*; import java.util.*; import org.apache.commons.logging.*; import org.rimudb.exception.*; import org.rimudb.statistics.*; /** * This class represents a transaction session. * * For batch operations, an internal queue is maintained. The queue contains batch statements * in the order they are submitted. One batch statement is used for each table. This * is necessary in order to ensure that batch statements get executed in the correct order. * * This means that if 10 batch inserts are executed for table_a, then another 10 batch inserts * are executed for table_b, and then another 10 batch inserts are executed for table_a, then * the queue will contain three prepared statements in order of inserts. When commit() is called * the three prepared statements will be executed one after another, preserving the order of * inserts. * * Obviously, if your application was coded differently, it would be more efficient to perform * all 20 batch inserts for table_a first, and then the 10 batch inserts for table_b. This would * only result in two prepared statements in the queue. * * In order to get the best batch performance, applications should attempt to perform all * the operations to one table and then all the operations to the next table. The worst performance * would occur if the application alternately executed one batch insert to table_a and then one * batch insert to table_b. This would result in no gain in performance over regular inserts. * * @author Simon Ritchie * */ public class Session { private final static Log log = LogFactory.getLog(Session.class); private Connection connection = null; private boolean originalAutoCommit; // Batches private LinkedList<BatchEntry> statementQueue = new LinkedList<BatchEntry>(); public final static int BATCH_NONE = 0; public final static int BATCH_INSERT = 1; public final static int BATCH_UPDATE = 2; public final static int BATCH_DELETE = 3; public Session(Connection databaseConnection) throws RimuDBException { this.connection = databaseConnection; try { originalAutoCommit = connection.getAutoCommit(); connection.setAutoCommit(false); } catch (SQLException e) { throw new RimuDBException(e, getClass().getName()); } } protected Connection getConnection() { return connection; } public void commit() throws RimuDBException { commit(false); } public void commit(boolean ignoreErrors) throws RimuDBException { try { if (getCurrentBatchSize() > 0) { // Execute all the batches in the queue while (statementQueue.size() > 0) { BatchEntry batchEntry = statementQueue.remove(); PreparedStatement ps = batchEntry.getPreparedStatement(); try { int[] results = ps.executeBatch(); } catch (BatchUpdateException be) { // Iterate through the batch counts to determine which data object had the error int[] updateCounts = be.getUpdateCounts(); // If the updateCounts match the number of records in the batch, then if (updateCounts.length == batchEntry.getSize()) { // Log a warning message with the entries that failed. for (int i = 0; i < updateCounts.length; i++) { if (updateCounts[i] == Statement.EXECUTE_FAILED) { DataObject dataObject = batchEntry.getDataObject(i); log.warn("Batch update of " + dataObject.getClass().getSimpleName() + " " + dataObject.getPrimaryWhereList() + " failed."); } } } if (!ignoreErrors) { throw be; } } // If this was a batch insert with generated keys if (batchEntry.getType() == BATCH_INSERT) { // If this table has generated keys and the database supports them then if (batchEntry.getTable().processesGeneratedKeys()) { // Populate the record Record[] records = batchEntry.getRecords(); batchEntry.getTable().populateGeneratedKeys(ps, records); } } ps.close(); } } // Commit the transaction getConnection().commit(); } catch (SQLException e) { throw new RimuDBException(e, getClass().getName()); } } /** * Return the current batch size. * * @return int */ public int getCurrentBatchSize() { if (statementQueue.size() == 0) return 0; return statementQueue.getLast().getSize(); } /** * Rollback the session. * * @throws RimuDBException */ public void rollback() throws RimuDBException { // Clear the batches on the prepared statements, and close the statements if (statementQueue.size() > 0) { try { while (statementQueue.size() > 0) { BatchEntry batchEntry = statementQueue.remove(); PreparedStatement ps = batchEntry.getPreparedStatement(); ps.clearBatch(); ps.close(); } } catch (SQLException e) { } } // rollback the transaction try { getConnection().rollback(); } catch (SQLException e) { throw new RimuDBException(e, getClass().getName()); } } /** * Close the session. * * @throws RimuDBException */ public void close() throws RimuDBException { if (statementQueue.size() > 0) { try { while (statementQueue.size() > 0) { BatchEntry batchEntry = statementQueue.remove(); PreparedStatement ps = batchEntry.getPreparedStatement(); ps.close(); } } catch (SQLException e) { } } try { getConnection().setAutoCommit(originalAutoCommit); getConnection().close(); } catch (SQLException e) { throw new RimuDBException(e, getClass().getName()); } } /** * Return any previously created prepared statement that we used for the table and type of batch. * * @param table Table * @param batchType int * @return PreparedStatement */ protected PreparedStatement getBatchStatement(Table table, int batchType) { // If the batch type and class are the same as what we last handled then return the last statement in the queue if (statementQueue.size() > 0 && statementQueue.getLast().isMatchingEntry(table, batchType)) { return statementQueue.getLast().getPreparedStatement(); } // Otherwise we don't have it return null; } /** * Save the prepared statement that we used for the table and batch type. * * @param table Table * @param batchStatement PreparedStatement * @param batchType int */ protected void setBatchStatement(Table table, PreparedStatement batchStatement, int batchType) { BatchEntry batchEntry = new BatchEntry(); batchEntry.setTable(table); batchEntry.setPreparedStatement(batchStatement); batchEntry.setType(batchType); // Save the new statement at the end of the queue statementQueue.add(batchEntry); } /** * Return statistics on the batches in the queue. * * @return BatchStatistic[] */ public BatchStatistic[] getBatchStatistics() { BatchStatistic[] stats = new BatchStatistic[statementQueue.size()]; for (int i = 0; i < stats.length; i++) { BatchEntry batchEntry = statementQueue.get(i); stats[i] = new BatchStatistic(batchEntry.getSize(), batchEntry.getType(), batchEntry.getTable().getDataObjectClass()); } return stats; } /** * Insert the data object within a batch. * * @param dataObject * @throws RimuDBException */ public void batchInsert(DataObject dataObject) throws RimuDBException { if (dataObject.isVirtual()) { throw new IllegalStateException("Cannot add a virtual data object"); } Table table = dataObject.getTable(); table.addRecordToBatch(this, dataObject.getRecord(), false); BatchEntry batchEntry = statementQueue.getLast(); batchEntry.addDataObject(dataObject); } public void batchDelete(DataObject dataObject) throws RimuDBException { if (dataObject.isVirtual()) { throw new IllegalStateException("Cannot delete a virtual data object"); } Table table = dataObject.getTable(); table.deleteByPrimaryKeyInBatch(this, dataObject.getPrimaryWhereList(), false); BatchEntry batchEntry = statementQueue.getLast(); batchEntry.addDataObject(dataObject); } public void batchUpdate(DataObject dataObject) throws RimuDBException { if (dataObject.isVirtual()) { throw new IllegalStateException("Cannot update a virtual data object"); } Table table = dataObject.getTable(); table.updateRecordInBatch(this, dataObject.getOriginalRecord(), dataObject.getRecord()); BatchEntry batchEntry = statementQueue.getLast(); batchEntry.addDataObject(dataObject); } }