All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sf.hajdbc.sync.DifferentialSynchronizationStrategy Maven / Gradle / Ivy

There is a newer version: 3.6.61
Show newest version
/*
 * HA-JDBC: High-Availability JDBC
 * Copyright (C) 2012  Paul Ferraro
 *
 * 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 .
 */
package net.sf.hajdbc.sync;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.regex.Pattern;

import net.sf.hajdbc.Database;
import net.sf.hajdbc.DatabaseCluster;
import net.sf.hajdbc.ExceptionType;
import net.sf.hajdbc.Messages;
import net.sf.hajdbc.SynchronizationStrategy;
import net.sf.hajdbc.TableProperties;
import net.sf.hajdbc.UniqueConstraint;
import net.sf.hajdbc.logging.Level;
import net.sf.hajdbc.logging.Logger;
import net.sf.hajdbc.logging.LoggerFactory;
import net.sf.hajdbc.util.Objects;
import net.sf.hajdbc.util.Resources;
import net.sf.hajdbc.util.Strings;

/**
 * Database-independent synchronization strategy that only updates differences between two databases.
 * This strategy is best used when there are few differences between the active database and the inactive database (i.e. barely out of sync).
 * The following algorithm is used:
 * 
    *
  1. Drop the foreign keys on the inactive database (to avoid integrity constraint violations)
  2. *
  3. For each database table: *
      *
    1. Drop the unique constraints on the table (to avoid integrity constraint violations)
    2. *
    3. Find the primary key(s) of the table
    4. *
    5. Query all rows in the inactive database table, sorting by the primary key(s)
    6. *
    7. Query all rows on the active database table
    8. *
    9. For each row in table: *
        *
      1. If primary key of the rows are the same, determine whether or not row needs to be updated
      2. *
      3. Otherwise, determine whether row should be deleted, or a new row is to be inserted
      4. *
      *
    10. *
    11. Re-create the unique constraints on the table (to avoid integrity constraint violations)
    12. *
    *
  4. *
  5. Re-create the foreign keys on the inactive database
  6. *
  7. Synchronize sequences
  8. *
* @author Paul Ferraro */ public class DifferentialSynchronizationStrategy implements SynchronizationStrategy, TableSynchronizationStrategy { private static final long serialVersionUID = -2785092229503649831L; static Logger logger = LoggerFactory.getLogger(DifferentialSynchronizationStrategy.class); private final SynchronizationStrategy strategy = new PerTableSynchronizationStrategy(this); private int fetchSize = 0; private int maxBatchSize = 100; private Pattern versionPattern = null; @Override public String getId() { return "diff"; } @Override public > void synchronize(SynchronizationContext context) throws SQLException { this.strategy.synchronize(context); } /** * {@inheritDoc} * @see net.sf.hajdbc.SynchronizationStrategy#init(net.sf.hajdbc.DatabaseCluster) */ @Override public > void init(DatabaseCluster cluster) { this.strategy.init(cluster); } /** * {@inheritDoc} * @see net.sf.hajdbc.SynchronizationStrategy#destroy(net.sf.hajdbc.DatabaseCluster) */ @Override public > void destroy(DatabaseCluster cluster) { this.strategy.destroy(cluster); } @Override public > void dropConstraints(SynchronizationContext context) throws SQLException { SynchronizationSupport support = context.getSynchronizationSupport(); support.dropForeignKeys(); support.dropUniqueConstraints(); } @Override public > void restoreConstraints(SynchronizationContext context) throws SQLException { SynchronizationSupport support = context.getSynchronizationSupport(); support.restoreUniqueConstraints(); support.restoreForeignKeys(); } /** * {@inheritDoc} * @see net.sf.hajdbc.SynchronizationStrategy#synchronize(net.sf.hajdbc.sync.SynchronizationContext) */ @Override public > void synchronize(SynchronizationContext context, TableProperties table) throws SQLException { String tableName = table.getName().getDMLName(); UniqueConstraint primaryKey = table.getPrimaryKey(); if (primaryKey == null) { throw new SQLException(Messages.PRIMARY_KEY_REQUIRED.getMessage(this.getClass().getName(), tableName)); } List primaryKeyColumns = primaryKey.getColumnList(); Collection columns = table.getColumns(); List nonPrimaryKeyColumns = new ArrayList(columns.size()); List versionColumns = new ArrayList(columns.size()); for (String column: columns) { if (!primaryKeyColumns.contains(column)) { // Try to find a version column if ((this.versionPattern != null) && this.versionPattern.matcher(column).matches()) { versionColumns.add(column); } nonPrimaryKeyColumns.add(column); } } // List of columns for select statement - starting with primary key List allColumns = new ArrayList(columns.size()); allColumns.addAll(primaryKeyColumns); allColumns.addAll(nonPrimaryKeyColumns); List selectColumns = allColumns; if (!versionColumns.isEmpty()) { selectColumns = new ArrayList(primaryKeyColumns.size() + versionColumns.size()); selectColumns.addAll(primaryKeyColumns); selectColumns.addAll(versionColumns); } // Retrieve table rows in primary key order final String selectSQL = String.format("SELECT %s FROM %s ORDER BY %s", Strings.join(selectColumns, Strings.PADDED_COMMA), tableName, Strings.join(primaryKeyColumns, Strings.PADDED_COMMA)); //$NON-NLS-1$ String primaryKeyWhereClause = Strings.join(new StringBuilder(), primaryKeyColumns, " = ? AND ").append(" = ?").toString(); //$NON-NLS-1$ String selectAllSQL = !versionColumns.isEmpty() ? String.format("SELECT %s FROM %s WHERE %s", Strings.join(nonPrimaryKeyColumns, Strings.PADDED_COMMA), tableName, primaryKeyWhereClause) : null; String deleteSQL = String.format("DELETE FROM %s WHERE %s", tableName, primaryKeyWhereClause); String insertSQL = String.format("INSERT INTO %s (%s) VALUES (%s)", tableName, Strings.join(allColumns, Strings.PADDED_COMMA), Strings.join(Collections.nCopies(allColumns.size(), Strings.QUESTION), Strings.PADDED_COMMA)); //$NON-NLS-1$ String updateSQL = !nonPrimaryKeyColumns.isEmpty() ? String.format("UPDATE %s SET %s = ? WHERE %s", tableName, Strings.join(nonPrimaryKeyColumns, " = ?, "), primaryKeyWhereClause) : null; Connection targetConnection = context.getConnection(context.getTargetDatabase()); final Statement targetStatement = targetConnection.createStatement(); try { targetStatement.setFetchSize(this.fetchSize); Callable callable = new Callable() { @Override public ResultSet call() throws SQLException { logger.log(Level.DEBUG, selectSQL); return targetStatement.executeQuery(selectSQL); } }; Future future = context.getExecutor().submit(callable); Connection sourceConnection = context.getConnection(context.getSourceDatabase()); Statement sourceStatement = sourceConnection.createStatement(); try { sourceStatement.setFetchSize(this.fetchSize); ResultSet sourceResultSet = sourceStatement.executeQuery(selectSQL); ResultSet targetResultSet = future.get(); PreparedStatement selectAllStatement = null; if (!versionColumns.isEmpty()) { logger.log(Level.DEBUG, selectAllSQL); selectAllStatement = targetConnection.prepareStatement(selectAllSQL); } try { logger.log(Level.DEBUG, deleteSQL); PreparedStatement deleteStatement = targetConnection.prepareStatement(deleteSQL); try { logger.log(Level.DEBUG, insertSQL); PreparedStatement insertStatement = targetConnection.prepareStatement(insertSQL); try { PreparedStatement updateStatement = null; if (!nonPrimaryKeyColumns.isEmpty()) { logger.log(Level.DEBUG, updateSQL); updateStatement = targetConnection.prepareStatement(updateSQL); } try { boolean hasMoreSourceResults = sourceResultSet.next(); boolean hasMoreTargetResults = targetResultSet.next(); int insertCount = 0; int updateCount = 0; int deleteCount = 0; while (hasMoreSourceResults || hasMoreTargetResults) { int compare = 0; if (!hasMoreSourceResults) { compare = 1; } else if (!hasMoreTargetResults) { compare = -1; } else { for (int i = 1; i <= primaryKeyColumns.size(); ++i) { Object sourceObject = sourceResultSet.getObject(i); Object targetObject = targetResultSet.getObject(i); // We assume that the primary keys column types are Comparable compare = compare(sourceObject, targetObject); if (compare != 0) { break; } } } if (compare > 0) { deleteStatement.clearParameters(); for (int i = 1; i <= primaryKeyColumns.size(); ++i) { int type = context.getDialect().getColumnType(table.getColumnProperties(allColumns.get(i - 1))); deleteStatement.setObject(i, targetResultSet.getObject(i), type); } deleteStatement.addBatch(); deleteCount += 1; if ((deleteCount % this.maxBatchSize) == 0) { deleteStatement.executeBatch(); deleteStatement.clearBatch(); } } else if (compare < 0) { insertStatement.clearParameters(); for (int i = 1; i <= primaryKeyColumns.size(); ++i) { int type = context.getDialect().getColumnType(table.getColumnProperties(allColumns.get(i - 1))); insertStatement.setObject(i, sourceResultSet.getObject(i), type); } if (versionColumns.isEmpty()) { for (int i = primaryKeyColumns.size() + 1; i <= allColumns.size(); ++i) { int type = context.getDialect().getColumnType(table.getColumnProperties(allColumns.get(i - 1))); Object object = context.getSynchronizationSupport().getObject(sourceResultSet, i, type); if (sourceResultSet.wasNull()) { insertStatement.setNull(i, type); } else { insertStatement.setObject(i, object, type); } } } else { if (selectAllStatement != null) { selectAllStatement.clearParameters(); for (int i = 1; i <= primaryKeyColumns.size(); ++i) { int type = context.getDialect().getColumnType(table.getColumnProperties(allColumns.get(i - 1))); selectAllStatement.setObject(i, sourceResultSet.getObject(i), type); } ResultSet selectAllResultSet = selectAllStatement.executeQuery(); for (int i = primaryKeyColumns.size() + 1; i <= allColumns.size(); ++i) { int type = context.getDialect().getColumnType(table.getColumnProperties(allColumns.get(i - 1))); Object object = context.getSynchronizationSupport().getObject(selectAllResultSet, i - primaryKeyColumns.size(), type); if (selectAllResultSet.wasNull()) { insertStatement.setNull(i, type); } else { insertStatement.setObject(i, object, type); } } } } insertStatement.addBatch(); insertCount += 1; if ((insertCount % this.maxBatchSize) == 0) { insertStatement.executeBatch(); insertStatement.clearBatch(); } } else if (updateStatement != null) // if (compare == 0) { updateStatement.clearParameters(); boolean updated = false; for (int i = primaryKeyColumns.size() + 1; i <= selectColumns.size(); ++i) { int type = context.getDialect().getColumnType(table.getColumnProperties(selectColumns.get(i - 1))); Object sourceObject = context.getSynchronizationSupport().getObject(sourceResultSet, i, type); Object targetObject = context.getSynchronizationSupport().getObject(targetResultSet, i, type); int index = i - primaryKeyColumns.size(); if (sourceResultSet.wasNull()) { updateStatement.setNull(index, type); updated |= !targetResultSet.wasNull(); } else { updateStatement.setObject(index, sourceObject, type); updated |= targetResultSet.wasNull(); updated |= !Objects.equals(sourceObject, targetObject); } } if (updated) { if (selectAllStatement != null) { selectAllStatement.clearParameters(); for (int i = 1; i <= primaryKeyColumns.size(); ++i) { int type = context.getDialect().getColumnType(table.getColumnProperties(allColumns.get(i - 1))); selectAllStatement.setObject(i, sourceResultSet.getObject(i), type); } ResultSet selectAllResultSet = selectAllStatement.executeQuery(); for (int i = primaryKeyColumns.size() + 1; i <= allColumns.size(); ++i) { int type = context.getDialect().getColumnType(table.getColumnProperties(allColumns.get(i - 1))); int index = i - primaryKeyColumns.size(); Object object = context.getSynchronizationSupport().getObject(selectAllResultSet, index, type); if (selectAllResultSet.wasNull()) { updateStatement.setNull(index, type); } else { updateStatement.setObject(index, object, type); } } } for (int i = 1; i <= primaryKeyColumns.size(); ++i) { int type = context.getDialect().getColumnType(table.getColumnProperties(allColumns.get(i - 1))); updateStatement.setObject(i + nonPrimaryKeyColumns.size(), targetResultSet.getObject(i), type); } updateStatement.addBatch(); updateCount += 1; if ((updateCount % this.maxBatchSize) == 0) { updateStatement.executeBatch(); updateStatement.clearBatch(); } } } if (hasMoreSourceResults && (compare <= 0)) { hasMoreSourceResults = sourceResultSet.next(); } if (hasMoreTargetResults && (compare >= 0)) { hasMoreTargetResults = targetResultSet.next(); } } if ((deleteCount % this.maxBatchSize) > 0) { deleteStatement.executeBatch(); } if ((insertCount % this.maxBatchSize) > 0) { insertStatement.executeBatch(); } if (updateStatement != null) { if ((updateCount % this.maxBatchSize) > 0) { updateStatement.executeBatch(); } } logger.log(Level.INFO, Messages.INSERT_COUNT.getMessage(), insertCount, tableName); logger.log(Level.INFO, Messages.UPDATE_COUNT.getMessage(), updateCount, tableName); logger.log(Level.INFO, Messages.DELETE_COUNT.getMessage(), deleteCount, tableName); } finally { if (updateStatement != null) { Resources.close(updateStatement); } } } finally { Resources.close(insertStatement); } } finally { Resources.close(deleteStatement); } } finally { if (selectAllStatement != null) { Resources.close(selectAllStatement); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new SQLException(e); } catch (ExecutionException e) { throw ExceptionType.SQL.getExceptionFactory().createException(e.getCause()); } finally { Resources.close(sourceStatement); } } finally { Resources.close(targetStatement); } } private static int compare(Object object1, Object object2) { @SuppressWarnings("unchecked") Comparable comparable = (Comparable) object1; return comparable.compareTo(object2); } /** * @return the fetchSize. */ public int getFetchSize() { return this.fetchSize; } /** * @param fetchSize the fetchSize to set. */ public void setFetchSize(int fetchSize) { this.fetchSize = fetchSize; } /** * @return Returns the maxBatchSize. */ public int getMaxBatchSize() { return this.maxBatchSize; } /** * @param maxBatchSize The maxBatchSize to set. */ public void setMaxBatchSize(int maxBatchSize) { this.maxBatchSize = maxBatchSize; } /** * @return the versionPattern */ public String getVersionPattern() { return (this.versionPattern != null) ? this.versionPattern.pattern() : null; } /** * @param versionPattern the versionPattern to set */ public void setVersionPattern(String versionPattern) { this.versionPattern = (versionPattern != null) ? Pattern.compile(versionPattern, Pattern.CASE_INSENSITIVE) : null; } }