DBMigration.java

  1. /*
  2.  * To change this license header, choose License Headers in Project Properties.
  3.  * To change this template file, choose Tools | Templates
  4.  * and open the template in the editor.
  5.  */
  6. package nz.co.gregs.dbvolution;

  7. import java.io.Serializable;
  8. import nz.co.gregs.dbvolution.internal.query.QueryDetails;
  9. import nz.co.gregs.dbvolution.databases.DBDatabase;
  10. import java.lang.reflect.Field;
  11. import java.lang.reflect.InvocationTargetException;
  12. import java.sql.SQLException;
  13. import java.util.ArrayList;
  14. import java.util.Arrays;
  15. import java.util.List;
  16. import java.util.stream.Collectors;
  17. import nz.co.gregs.dbvolution.actions.DBMigrationAction;
  18. import nz.co.gregs.dbvolution.datatypes.QueryableDatatype;
  19. import nz.co.gregs.dbvolution.exceptions.*;
  20. import nz.co.gregs.dbvolution.expressions.*;
  21. import nz.co.gregs.dbvolution.query.*;

  22. public class DBMigration<M extends DBRow> extends RowDefinition implements Serializable{

  23.     private static final long serialVersionUID = 1L;

  24.     private final M mapper;
  25.     private final ArrayList<DBRow> optionalTables = new ArrayList<>();
  26.     private final DBDatabase database;
  27.    
  28.     private final ArrayList<SortProvider> sortColumns = new ArrayList<>();
  29.     Boolean cartesian = false;
  30.     Boolean blank = false;


  31.     private DBMigration(DBDatabase database, M migrationMapper) {
  32.         this.mapper = DBRow.copyDBRow(migrationMapper);
  33.         this.database = database.copy();
  34.     }

  35.     public DBMigration<M> copy() {
  36.         DBMigration<M> copiedMigration = using(database, mapper);
  37.         copiedMigration.optionalTables.addAll(optionalTables);
  38.         copiedMigration.sortColumns.addAll(sortColumns);
  39.         copiedMigration.cartesian = cartesian;
  40.         copiedMigration.blank = blank;
  41.         return copiedMigration;
  42.     }

  43.     /**
  44.      * Create a migration that uses the mapper provided.
  45.      *
  46.      * <p>
  47.      * Mapper uses a subclass of a particular DBRow to create rows for that DBRow
  48.      * and insert them into the table.</p>
  49.      *
  50.      * <p>
  51.      * Central to this concept is using other existing data in table, represented
  52.      * by their own DBRow classes, transformed using DBvolution expressions</p>
  53.      * <pre>
  54.      * public static class MigrateToFight extends Fight {
  55.      * public Villain baddy = new Villain();
  56.      * public Hero goody = new Hero();
  57.      * {
  58.      * baddy.name.permittedPattern("Dr%");
  59.      * hero = goody.column(goody.name).asExpressionColumn();
  60.      * villain = baddy.column(baddy.name).asExpressionColumn();
  61.      * }
  62.      * }
  63.      * database.createTable(new Fight());
  64.      * DBMigration migration = DBMigration.using(new MigrateToFight());
  65.      * migration.createAllRows(database);
  66.      * </pre>
  67.      *
  68.      * @param <MAPPER> the transformation to apply
  69.      * @param database the database to work with
  70.      * @param migrationMapper the transformation class
  71.      * @return a DBMigration object
  72.      */
  73.     public static <MAPPER extends DBRow> DBMigration<MAPPER> using(DBDatabase database, MAPPER migrationMapper) {
  74.         return new DBMigration<MAPPER>(database, migrationMapper);
  75.     }

  76.     public DBMigration<M> addTablesAndExpressions(DBQuery query) {
  77.         Field[] fields = mapper.getClass().getFields();
  78.         if (fields.length == 0) {
  79.             throw new UnableToAccessDBMigrationFieldException(this, null);
  80.         }
  81.         for (Field field : fields) {
  82.             field.setAccessible(true);
  83.             final Object value;
  84.             try {
  85.                 value = field.get(mapper);
  86.                 if (value != null && DBRow.class.isAssignableFrom(value.getClass())) {
  87.                     if (value instanceof DBRow) {
  88.                         final DBRow dbRow = (DBRow) value;
  89.                         dbRow.removeAllFieldsFromResults();
  90.                         if (optionalTables.contains(dbRow)) {
  91.                             query.addOptional(dbRow);
  92.                         } else {
  93.                             query.add(dbRow);
  94.                         }
  95.                     }
  96.                 } else if (value != null && QueryableDatatype.class.isAssignableFrom(value.getClass())) {
  97.                     final QueryableDatatype<?> qdtValue = (QueryableDatatype) value;
  98.                     if ((value instanceof QueryableDatatype) && qdtValue.hasColumnExpression()) {
  99.                         query.addExpressionColumn(value, qdtValue);
  100.                         final DBExpression[] columnExpressions = qdtValue.getColumnExpression();
  101.                         for (DBExpression columnExpression : columnExpressions) {
  102.                             if (!columnExpression.isAggregator()) {
  103.                                 query.addGroupByColumn(value, columnExpression);
  104.                             }
  105.                         }
  106.                     }
  107.                 }
  108.             } catch (IllegalArgumentException | IllegalAccessException ex) {
  109.                 throw new UnableToAccessDBMigrationFieldException(this, field, ex);
  110.             }
  111.         }
  112.         return this;
  113.     }

  114.     @SuppressWarnings("unchecked")
  115.     M createInstanceOfMappingTarget() throws InstantiationException, IllegalAccessException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
  116.         Class<? extends DBRow> aClass = mapper.getClass();
  117.         return (M) aClass.getConstructor().newInstance();
  118.     }

  119.     private M getMappedTarget(DBQueryRow row) {
  120.         try {
  121.             M newTarget = createInstanceOfMappingTarget();
  122.             Field[] fields = mapper.getClass().getFields();
  123.             for (Field field : fields) {
  124.                 field.setAccessible(true);
  125.                 final Object value;
  126.                 try {
  127.                     value = field.get(mapper);
  128.                     if (value != null && DBRow.class.isAssignableFrom(value.getClass())) {
  129.                         if (value instanceof DBRow) {
  130.                             DBRow gotDefinedRow = row.get((DBRow) value);
  131.                             try {
  132.                                 Field targetField = newTarget.getClass().getField(field.getName());
  133.                                 targetField.set(newTarget, gotDefinedRow);
  134.                             } catch (NoSuchFieldException ex) {
  135.                             }
  136.                         }
  137.                     } else if (value != null && QueryableDatatype.class.isAssignableFrom(value.getClass())) {
  138.                         if ((value instanceof QueryableDatatype) && ((QueryableDatatype) value).hasColumnExpression()) {
  139.                             final QueryableDatatype<?> expressionColumnValue = row.getExpressionColumnValue(value);
  140.                             try {
  141.                                 Field targetField = newTarget.getClass().getField(field.getName());
  142.                                 targetField.set(newTarget, expressionColumnValue);
  143.                             } catch (NoSuchFieldException ex) {
  144.                             }
  145.                         }
  146.                     }
  147.                 } catch (IllegalArgumentException ex) {
  148.                     throw new UnableToSetDBMigrationFieldException(newTarget, field, ex);
  149.                 } catch (IllegalAccessException ex) {
  150.                     throw new UnableToAccessDBMigrationFieldException(newTarget, field, ex);
  151.                 }
  152.             }
  153.             return newTarget;
  154.         } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | IllegalArgumentException | InvocationTargetException ex) {
  155.             throw new UnableToInstantiateDBMigrationSubclassException(this, ex);
  156.         }
  157.     }

  158.     /**
  159.      * Gets all the migrated rows using conditions in the DBMigration and the
  160.      * supplied examples.
  161.      *
  162.      * @param extraExamples extra rows defining additional criteria
  163.      * @return a list of DBReport instances representing the results of the report
  164.      * query. 1 Database exceptions may be thrown
  165.      * @throws java.sql.SQLException java.sql.SQLException
  166.      * @throws nz.co.gregs.dbvolution.exceptions.AccidentalBlankQueryException
  167.      * thrown if no conditions have been set and blank queries have not been
  168.      * explicitly allowed
  169.      */
  170.     public List<M> getAllRows(DBRow... extraExamples) throws SQLException, AccidentalCartesianJoinException, AccidentalBlankQueryException {
  171.         DBQuery query = getDBQuery(extraExamples);
  172.         List<DBQueryRow> allRows = query.getAllRows();
  173.         List<M> reportRows = getInsertedRowsFromQueryResults(allRows);
  174.         return reportRows;
  175.     }

  176.     @Override
  177.     public String toString() {
  178.         StringBuilder str = new StringBuilder();
  179.         Field[] fields = this.getClass().getFields();
  180.         for (Field field : fields) {
  181.             field.setAccessible(true);
  182.             final Object value;
  183.             try {
  184.                 value = field.get(this);
  185.                 if (value != null && DBRow.class.isAssignableFrom(value.getClass())) {
  186.                     if (value instanceof DBRow) {
  187.                         final DBRow dbRow = (DBRow) value;
  188.                         str.append(dbRow.toString());
  189.                     }
  190.                 } else if (value != null && QueryableDatatype.class.isAssignableFrom(value.getClass())) {
  191.                     if ((value instanceof QueryableDatatype)) {
  192.                         QueryableDatatype<?> qdt = (QueryableDatatype) value;
  193.                         str.append(field.getName()).append(": ").append(qdt.toString()).append(" ");
  194.                     }
  195.                 }
  196.             } catch (IllegalArgumentException | IllegalAccessException ex) {
  197.                 throw new UnableToAccessDBMigrationFieldException(this, field, ex);
  198.             }
  199.         }
  200.         return str.toString();
  201.     }

  202.     /**
  203.      * Gets all the report rows of the migration limited by the supplied example
  204.      * rows.
  205.      *
  206.      * <p>
  207.      * All supplied rows should be using a DBRow subclass that is included in the
  208.      * report.
  209.      *
  210.      * <p>
  211.      * Builtin report limitation will be used, the example rows supply further
  212.      * details for constraining the report.
  213.      *
  214.      * <p>
  215.      * This method allows you to create generic reports and apply dynamic
  216.      * limitations such as date ranges, department name, and other highly variable
  217.      * parameters.
  218.      *
  219.      * @param rows rows
  220.      * @return a list of DBReport instances representing the results of the report
  221.      * query. 1 Database exceptions may be thrown
  222.      * @throws java.sql.SQLException java.sql.SQLException
  223.      * @throws nz.co.gregs.dbvolution.exceptions.AccidentalBlankQueryException
  224.      * thrown if no conditions have been set and blank queries have not been
  225.      * explicitly allowed
  226.      */
  227.     public List<M> getRows(DBRow... rows) throws SQLException, AccidentalCartesianJoinException, AccidentalBlankQueryException {
  228.         DBQuery query = getDBQuery(rows);
  229.         List<DBQueryRow> allRows = query.getAllRows();
  230.         List<M> reportRows = getInsertedRowsFromQueryResults(allRows);
  231.         return reportRows;
  232.     }

  233.     /**
  234.      * Gets all the report rows of this DBMigration limited by the supplied
  235.      * example rows but reduce the result to only those that match the conditions.
  236.      *
  237.      * <p>
  238.      * All conditions should only reference the fields/column of the DBMigration.
  239.      *
  240.      * <p>
  241.      * All supplied rows should be using a DBRow subclass that is included in the
  242.      * report.
  243.      *
  244.      * <p>
  245.      * Built-in report limitation will be used, the example rows supply further
  246.      * details for constraining the report.
  247.      *
  248.      * <p>
  249.      * This method allows you to create generic reports and apply dynamic
  250.      * limitations such as date ranges, department name, and other highly variable
  251.      * parameters.
  252.      *
  253.      * @param rows rows example rows that provide extra criteria
  254.      * @param conditions the conditions that will be supplied to the WHERE or
  255.      * HAVING clause of the query
  256.      * @return a list of DBReport instances representing the results of the report
  257.      * query
  258.      * @throws java.sql.SQLException Database exceptions may be thrown
  259.      * @throws nz.co.gregs.dbvolution.exceptions.AccidentalBlankQueryException
  260.      * thrown if no conditions have been set and blank queries have not been
  261.      * explicitly allowed
  262.      */
  263.     public List<M> getRowsHaving(DBRow[] rows, BooleanExpression... conditions) throws SQLException, AccidentalCartesianJoinException, AccidentalBlankQueryException {
  264.         DBQuery query = getDBQuery(rows);
  265.         List<M> reportRows;
  266.         List<DBQueryRow> allRows = query.addConditions(conditions).getAllRows();
  267.         reportRows = getInsertedRowsFromQueryResults(allRows);
  268.         return reportRows;
  269.     }

  270.     private List<M> getInsertedRowsFromQueryResults(List<DBQueryRow> allRows) {
  271.         List<M> reportRows = allRows
  272.                 .stream()
  273.                 .map((t) -> getMappedTarget(t))
  274.                 .collect(Collectors.toList());
  275.         return reportRows;
  276.     }

  277.     /**
  278.      * Generates and returns the actual SQL to be used by this DBQueryInsert to
  279.      * select the rows to insert.
  280.      *
  281.      * <p>
  282.      * Good for debugging and great for DBAs, this is how you find out what
  283.      * DBvolution is really doing.
  284.      *
  285.      * <p>
  286.      * Generates the SQL query for retrieving the objects but does not execute the
  287.      * SQL. Use
  288.      * {@link #getAllRows(nz.co.gregs.dbvolution.DBRow...) the getAllRows method}
  289.      * to retrieve the rows.
  290.      *
  291.      * <p>
  292.      * See also
  293.      * {@link #getSQLForCount(nz.co.gregs.dbvolution.DBRow...)  }
  294.      *
  295.      * @param rows additional conditions to apply to the report.
  296.      * @return a String of the SQL that will be used by this DBQuery. 1 Database
  297.      * exceptions may be thrown
  298.      */
  299.     public String getSQLForQuery(DBRow... rows) {
  300.         DBQuery query = getDBQuery(rows);
  301.         return query.getSQLForQuery();
  302.     }

  303.     /**
  304.      * Generates and returns the actual SQL to be used by this DBQueryInsert to
  305.      * insert the queried rows.
  306.      *
  307.      * <p>
  308.      * Good for debugging and great for DBAs, this is how you find out what
  309.      * DBvolution is really doing.
  310.      *
  311.      * <p>
  312.      * Generates the SQL query for retrieving the objects but does not execute the
  313.      * SQL. Use
  314.      * {@link #getAllRows(nz.co.gregs.dbvolution.DBRow...) the getAllRows method}
  315.      * to retrieve the rows.
  316.      *
  317.      * <p>
  318.      * See also
  319.      * {@link #getSQLForCount(nz.co.gregs.dbvolution.DBRow...)  }
  320.      *
  321.      * @param rows additional conditions to apply to the report.
  322.      * @return a String of the SQL that will be used by this DBQuery. 1 Database
  323.      * exceptions may be thrown
  324.      */
  325.     public String getSQLForInsert(DBRow... rows) {
  326.         DBMigrationAction<M> action = getDBMigrationAction(rows);
  327.         ArrayList<String> sqlStatements = action.getSQLStatements(database);
  328.         if (sqlStatements.size() > 0) {
  329.             return sqlStatements.get(0);
  330.         } else {
  331.             return "";
  332.         }
  333.     }

  334.     /**
  335.      * Returns the SQL query that will used to count the rows returned for the
  336.      * supplied DBReport
  337.      *
  338.      * <p>
  339.      * Use this method to check the SQL that will be executed during
  340.      * {@link DBReport#count(nz.co.gregs.dbvolution.databases.DBDatabase, nz.co.gregs.dbvolution.DBReport, nz.co.gregs.dbvolution.DBRow...)  the count method}
  341.      *
  342.      * @param rows additional conditions to be applied.
  343.      * @return a String of the SQL query that will be used to count the rows
  344.      * returned by this report 1 Database exceptions may be thrown
  345.      * @throws java.sql.SQLException java.sql.SQLException
  346.      */
  347.     public String getSQLForCount(DBRow... rows) throws SQLException {
  348.         DBQuery query = getDBQuery(rows);
  349.         return query.getSQLForCount();
  350.     }

  351.     /**
  352.      * Count the rows on the database without retrieving the rows.
  353.      *
  354.      * <p>
  355.      * Creates a
  356.      * {@link #getSQLForCount(nz.co.gregs.dbvolution.DBRow...)   count query}
  357.      * for the report and conditions and retrieves the number of rows that would
  358.      * have been returned had
  359.      * {@link #getAllRows(nz.co.gregs.dbvolution.DBRow...) getAllRows method}
  360.      * been called.
  361.      *
  362.      * @param rows additional conditions for the query.
  363.      * @return the number of rows that have or will be retrieved. 1 Database
  364.      * exceptions may be thrown
  365.      * @throws java.sql.SQLException java.sql.SQLException
  366.      */
  367.     public Long count(DBRow... rows) throws SQLException, AccidentalCartesianJoinException, AccidentalBlankQueryException {
  368.         DBQuery setUpQuery = getDBQuery(rows);
  369.         return setUpQuery.count();
  370.     }

  371.     /**
  372.      * Sets the sort order of DBReport (field and/or method) by the given column
  373.      * providers.
  374.      *
  375.      * <p>
  376.      * For example the following code snippet will sort by just the name column:
  377.      * <pre>
  378.      * CustomerReport customers = ...;
  379.      * customers.setSortOrder(customers.column(customers.name));
  380.      * </pre>
  381.      *
  382.      * @param columns a list of columns to sort the query by.
  383.      * @return this DBReport instance
  384.      */
  385. //  public DBMigration<M> setSortOrder(SortProvider... columns) {
  386. //      sortColumns.clear();
  387. //      sortColumns.addAll(Arrays.asList(columns));
  388. //      return this;
  389. //  }

  390.     /**
  391.      * Sets the sort order of migration (field and/or method) by the given column
  392.      * providers.
  393.      *
  394.      * <p>
  395.      * ONLY USE FIELDS FROM THE SAME INSTANCE.
  396.      * <p>
  397.      * For example the following code snippet will sort by the name and
  398.      * accountNumber columns:
  399.      * <pre>
  400.      * CustomerReport customers = ...;
  401.      * customers.setSortOrder(customers.name, customers.accountNumber);
  402.      * </pre>
  403.      *
  404.      * @param columns a list of columns to sort the query by.
  405.      * @return this DBReport instance
  406.      */
  407. //  public DBMigration<M> setSortOrder(QueryableDatatype<?>... columns) {
  408. //      List<SortProvider> columnProviders = new ArrayList<>();
  409. //      for (QueryableDatatype<?> qdt : columns) {
  410. //          final ColumnProvider expr = this.column(qdt);
  411. //          columnProviders.add(expr.getSortProvider());
  412. //      }
  413. //      sortColumns.addAll(columnProviders);
  414. //      return this;
  415. //  }

  416.     /**
  417.      * Add the rows as optional tables in the query.
  418.      *
  419.      * <p>
  420.      * Optional tables are joined to the query using an outer join and results are
  421.      * returned regardless of whether there is a matching row in the optional
  422.      * table.</p>
  423.      *
  424.      * <p>
  425.      * Optional rows may contain only DBNull values, that is there getValue()
  426.      * method will return NULL and isDBNull() will be true.</p>
  427.      *
  428.      * @param examples additional tables to include in the query if possible.
  429.      */
  430.     public void addAsOptionalTables(DBRow... examples) {
  431.         optionalTables.addAll(Arrays.asList(examples));
  432.     }

  433.     DBQuery getDBQuery(DBRow... rows) {
  434.         DBQuery query = database.getDBQuery();
  435.         query.setBlankQueryAllowed(blank);
  436.         query.setCartesianJoinsAllowed(cartesian);
  437.         addTablesAndExpressions(query);
  438.         query.addExtraExamples(rows);
  439.         query.setSortOrder(this.getSortColumns());
  440.         return query;
  441.     }

  442.     /**
  443.      * Returns the list of sort columns
  444.      *
  445.      * @return the sortColumns
  446.      */
  447.     public SortProvider[] getSortColumns() {
  448.         return sortColumns.toArray(new SortProvider[]{});
  449.     }

  450.     /**
  451.      * Suppresses Cartesian join error protection.
  452.      *
  453.      * <p>
  454.      * DBvolution protects you using accidental Cartesian joins but use this
  455.      * function if a Cartesian is required.</p>
  456.      *
  457.      * <p>
  458.      * Cartesian joins occur when there is no connection between 2 (or more)
  459.      * tables. Normally all tables are connect by a chain of relationships,
  460.      * usually primary key to foreign key.</p>
  461.      * <p>
  462.      * Sometimes a connection is missed: for instance 2 unrelated tables are being
  463.      * compared by price, but the price relating expression has not been added. In
  464.      * this case DBvolution will throw an {@link AccidentalCartesianJoinException}
  465.      * and abort the query. This exception avoids creating a probably massive
  466.      * dataset that will reduce database and network performance
  467.      * significantly.</p>
  468.      * <p>
  469.      * However there are valid cases for a Cartesian join: finding all possible
  470.      * combinations of cake and coffee for instance.</p>
  471.      * <p>
  472.      * If you are sure you need a Cartesian join, use this method to avoid the
  473.      * error-checking and the {@link AccidentalCartesianJoinException}</p>
  474.      *
  475.      * @param setting True if you need a Cartesian join in this DBQueryInsert.
  476.      * @return this DBQueryInsert object
  477.      */
  478.     public DBMigration<M> setCartesianJoinAllowed(Boolean setting) {
  479.         cartesian = setting;
  480.         return this;
  481.     }

  482.     /**
  483.      * Change the Default Setting of Disallowing Blank Queries
  484.      *
  485.      * <p>
  486.      * A common mistake is creating a query without supplying criteria and
  487.      * accidentally retrieving a huge number of rows.
  488.      *
  489.      * <p>
  490.      * DBvolution detects this situation and, by default, throws a
  491.      * {@link nz.co.gregs.dbvolution.exceptions.AccidentalBlankQueryException AccidentalBlankQueryException}
  492.      * when it happens.
  493.      *
  494.      * <p>
  495.      * To change this behaviour, and allow blank queries, call
  496.      * {@code setBlankQueriesAllowed(true)}.
  497.      *
  498.      * @param setting - TRUE to allow blank queries, FALSE to return it to the
  499.      * default setting.
  500.      * @return this DBQueryInsert instance
  501.      */
  502.     public DBMigration<M> setBlankQueryAllowed(Boolean setting) {
  503.         blank = setting;
  504.         return this;
  505.     }

  506.     public void createAllRows(DBRow... extraExamples) throws SQLException {
  507.         DBMigrationAction<M> migrate = getDBMigrationAction(extraExamples);
  508.         migrate.migrate(database);
  509.     }

  510.     private DBMigrationAction<M> getDBMigrationAction(DBRow[] extraExamples) {
  511.         return new DBMigrationAction<>(this, mapper, extraExamples);
  512.     }

  513.     /**
  514.      * Perform a migration validation using DBMigrationValidation.
  515.      *
  516.      * <p>
  517.      * Validation will process all the available rows and try migrate them or to
  518.      * identify why they were not migrated. FAILURE TO MIGRATE MAY BE CORRECT
  519.      * BEHAVIOUR. Carefully check the results to determine if there are issues
  520.      * with the migration or the data.</p>
  521.      *
  522.      * @param extraExamples extra examples
  523.      * @return result from the migration validation
  524.      * @throws SQLException if the database has an issue with the validation script
  525.      */
  526.     public DBMigrationValidation.Results validateAllRows(DBRow... extraExamples) throws SQLException {

  527.         DBMigrationValidation<M> validate = new DBMigrationValidation<>(this, mapper, extraExamples);
  528.         return validate.validate(database);
  529.     }

  530.     QueryDetails getQueryDetails() {
  531.         return this.getDBQuery().getQueryDetails();
  532.     }

  533.     public boolean getCartesianJoinsAllowed() {
  534.         return cartesian;
  535.     }

  536.     public boolean getBlankQueryAllowed() {
  537.         return blank;
  538.     }

  539.     public M getMapper() {
  540.         return mapper;
  541.     }
  542.    
  543.     public List<DBRow> getOptionalTables(){
  544.         if(optionalTables.isEmpty()){
  545.             return new ArrayList<DBRow>();
  546.         }
  547.         return Arrays.asList((DBRow[]) optionalTables.toArray());
  548.     }

  549. }