DBAction.java
/*
* Copyright 2013 Gregory Graham.
*
* Licensed 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.
*/
package nz.co.gregs.dbvolution.actions;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.List;
import nz.co.gregs.dbvolution.DBRow;
import nz.co.gregs.dbvolution.databases.DBDatabase;
import nz.co.gregs.dbvolution.databases.DBStatement;
import nz.co.gregs.dbvolution.databases.QueryIntention;
import nz.co.gregs.dbvolution.databases.definitions.DBDefinition;
import nz.co.gregs.dbvolution.datatypes.QueryableDatatype;
import nz.co.gregs.dbvolution.exceptions.DBRuntimeException;
import nz.co.gregs.dbvolution.exceptions.UnexpectedNumberOfRowsException;
import nz.co.gregs.dbvolution.internal.properties.PropertyWrapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* DBAction encapsulates the concept of permanent changes to the database.
*
* <p>
* All DBActions are an immutable encapsulation of the action and row at the
* point in time. A DBAction will perform the same action, given the same
* DBDatabase, no matter what has happened since the creation of the DBAction.
*
* <p>
* Usually you should use the methods {@link DBDatabase#delete(nz.co.gregs.dbvolution.DBRow...) }, {@link DBDatabase#update(nz.co.gregs.dbvolution.DBRow...) }, {@link DBDatabase#insert(nz.co.gregs.dbvolution.DBRow...)
* } to automatically execute the DBActions, update any DBRows that need
* updating, and return the performed DBActions as a DBActionList.
*
* <p>
* However DBAction and it's subclasses provide more features for scripting
* particularly {@link DBDelete#getDeletes(nz.co.gregs.dbvolution.databases.DBDatabase, nz.co.gregs.dbvolution.DBRow...) }, {@link DBUpdate#getUpdates(nz.co.gregs.dbvolution.DBRow...)
* }, and {@link DBInsert#getInserts(nz.co.gregs.dbvolution.DBRow...) },
* allowing a series of changes to be created then executed in a single batch.
*
* @author Gregory Graham
*/
public abstract class DBAction implements Serializable {
private static final long serialVersionUID = 1L;
private static final Log LOG = LogFactory.getLog(DBInsert.class);
final DBRow row;
private RefetchRequirement refetchStatus = RefetchRequirement.REFETCH;
protected final QueryIntention intention;
/**
* Standard action constructor.
*
* <p>
* Saves a copy of the row to ensure immutability.</p>
*
* @param <R> the table that this action applies to.
* @param row the row or example that this action applies to.
* @param intent the specific intention of this action, a description of what
* is expected to occur
*/
public <R extends DBRow> DBAction(R row, QueryIntention intent) {
super();
if (row != null) {
this.row = DBRow.copyDBRow(row);
} else {
this.row = row;
}
this.intention = intent;
}
public QueryIntention getIntent() {
return intention;
}
/**
* Returns a DBActionList containing the changes required to revert the
* DBAction.
*
* <p>
* Every action has an opposite reaction. This method supplies the actions
* require to revert the change enacted by the action.
*
* <p>
* Revert actions are tricky to implement correctly, so be sure to check that
* the revert will produce the desired result.
*
* @return a list of all the actions required to revert this action in the
* order they need to enacted.
*/
protected abstract DBActionList getRevertDBActionList();
/**
* Returns a copy of the row supplied during creation.
*
* @return the row
*/
public DBRow getRow() {
return DBRow.copyDBRow(row);
}
/**
* Returns a string that can be used in the WHERE clause to identify the rows
* affected by this DBAction.
*
* <p>
* Used internally during UPDATE and INSERT.</p>
*
* @param row the row that will be used in the method
* @param db the database to execute the DBAction on
*
* @return a string representing the
*/
protected String getPrimaryKeySQL(DBDatabase db, DBRow row) {
final DBDefinition definition = db.getDefinition();
StringBuilder sqlString = new StringBuilder();
List<QueryableDatatype<?>> primaryKeys = row.getPrimaryKeys();
String separator = "(";
for (QueryableDatatype<?> pk : primaryKeys) {
var wrapper = row.getPropertyWrapperOf(pk);
String pkValue = (pk.hasChanged() ? pk.getPreviousSQLValue(definition) : pk.toSQLString(definition));
sqlString.append(separator)
.append(definition.formatColumnName(wrapper.columnName()))
.append(definition.getEqualsComparator())
.append(pkValue);
separator = definition.beginAndLine();
}
return sqlString.append(")").toString();
}
/**
* Returns a list of the SQL statements that this DBAction will produce for
* the specified database.
*
* <p>
* Actions happen all by themselves but when you want to know what will
* actually happen, use this method to get a complete list of all the SQL
* required.
*
* @param db the database that the SQL must be appropriate for.
* @return the list of SQL strings that equates to this action.
*/
public abstract List<String> getSQLStatements(DBDatabase db);
/**
* Performs the DB execute and returns a list of all actions performed in the
* process.
*
* <p>
* The supplied row will be changed by the action in an appropriate way,
* however the Action will contain an unchanged and unchangeable copy of the
* row for internal use.
*
* @param db the target database.
* @return The complete list of all actions performed to complete this action
* on the database
* @throws SQLException Database operations may throw SQLExceptions
*/
public abstract DBActionList execute(DBDatabase db) throws SQLException;
public boolean requiresRunOnIndividualDatabaseBeforeCluster() {
return false;
}
public boolean runOnDatabaseDuringCluster(DBDatabase initialDatabase, DBDatabase next) {
return true;
}
protected void refetchIfClusterRequires(DBDatabase db, DBRow originalRow) {
try {
if (refetchNeeded()) {
if (originalRow.hasAutomaticValueFields()) {
if (originalRow.getPrimaryKeys().size() > 0) {
updateRefetchRequirementForOtherDatabases();
DBRow example = DBRow.getPrimaryKeyExample(originalRow);
DBRow newRow = db
.getDBTable(example)
.setQueryLabel("AUTOMATIC REFETCH")
.getOnlyRow();
List<PropertyWrapper<?, ?, ?>> props = originalRow.getColumnPropertyWrappers();
props.stream().filter(p -> p != null).forEach(p -> p.copyFromRowToOtherRow(newRow, originalRow));
}
}
}
} catch (SQLException | DBRuntimeException ex) {
LOG.fatal(null, ex);
}
}
private boolean refetchNeeded() {
return RefetchRequirement.REFETCH.equals(refetchStatus);
}
protected void updateRefetchRequirementForOtherDatabases() {
setRefetchStatus(RefetchRequirement.REFETCH);
}
protected void setRefetchStatus(RefetchRequirement refetchStatus) {
this.refetchStatus = refetchStatus;
}
protected void executeOnStatement(DBDatabase db) throws SQLException {
try (final DBStatement statement = db.getDBStatement()) {
for (String sql : getSQLStatements(db)) {
statement.execute(getIntent(), sql);
}
}
}
protected void executeOnStatement(DBDatabase db, DBActionList actions) throws SQLException {
try (final DBStatement statement = db.getDBStatement()) {
for (String sql : actions.getSQL(db)) {
statement.execute(getIntent(), sql);
}
}
}
public DBActionList execute2(DBDatabase db) throws SQLException {
DBActionList actions = prepareActionList(db);
prepareRollbackData(db, actions);
executeOnStatement(db,actions);
return actions;
}
protected DBActionList prepareActionList(DBDatabase db) throws SQLException, DBRuntimeException {
throw new UnsupportedOperationException("Not supported yet.");
}
protected void prepareRollbackData(DBDatabase db, DBActionList actions) throws SQLException, DBRuntimeException {
throw new UnsupportedOperationException("Not supported yet.");
}
public static enum RefetchRequirement {
REFETCH,
DO_NOT_REFETCH
}
}