org.rimudb.Session.java Source code

Java tutorial

Introduction

Here is the source code for org.rimudb.Session.java

Source

/*
 * 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);
    }
}