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

io.convergence_platform.common.database.BaseDatabaseMigratorService Maven / Gradle / Ivy

Go to download

Holds the common functionality needed by all Convergence Platform-based services written in Java.

The newest version!
package io.convergence_platform.common.database;

import io.convergence_platform.common.dag.WorkflowDAG;
import io.convergence_platform.common.dag.WorkflowStepInfo;
import io.convergence_platform.common.database.blueprint_formatters.h2.*;
import io.convergence_platform.common.database.blueprint_formatters.mysql.*;
import io.convergence_platform.common.database.blueprint_formatters.postgres.*;
import io.convergence_platform.common.database.migrations.SqlDialectFormatter;

import java.sql.*;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public abstract class BaseDatabaseMigratorService implements IDatabaseMigratorService {
    protected abstract String getDatabaseURL();

    protected abstract String getDatabaseUserName();

    protected abstract String getDatabasePassword();

    public void migrateDatabase() {
//        logger.info("Started migrating database");
        List migrations = getAllDatabasePreparationSteps();
//        logger.info(String.format("Found %d migrations:", migrations.size()));
//        for (int i = 0; i < migrations.size(); i++) {
//            logger.info(String.format("   %d. %s", i + 1, migrations.get(i).name()));
//        }
        List appliedDatabaseMigrations = getListOfMigrations();
        SqlDialectFormatter formatters = initializeFormatters();

        try (Connection connection = DriverManager.getConnection(getDatabaseURL(), getDatabaseUserName(), getDatabasePassword())) {
            for (IDatabasePreparationStep migration : migrations) {
                if (!hasBeenApplied(migration, appliedDatabaseMigrations)) {
//                    logger.info("Start applying migration: " + migration.name());
                    applyMigration(migration, connection, formatters);
                }
            }

            onAfterAppliedMigrations(connection);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    protected void onAfterAppliedMigrations(Connection connection) {

    }

    private String getMigrationSQL(IDatabasePreparationStep migration, SqlDialectFormatter formatters) {
        String result = null;

        if (migration instanceof IDatabaseMigration) {
            result = ((IDatabaseMigration) migration).getMigrationDDL(formatters);
        } else if (migration instanceof IDatabaseSeed) {
            List entries = ((IDatabaseSeed) migration).getSeeds();
            result = formatters.insertSeedsFormatter.toSQL(entries);
        } else {
            throw new RuntimeException();
        }

        return result;
    }

    private SqlDialectFormatter initializeFormatters() {
        SqlDialectFormatter result = new SqlDialectFormatter();

        if (getDatabaseURL().startsWith("jdbc:h2:mem:")) {
            // H2 DB for test
            result.createTableFormatter = new H2InMemoryTableBlueprintFormatter();
            result.createRelationFormatter = new H2InMemoryTableRelationFormatter();
            result.insertSeedsFormatter = new H2InMemoryDatabaseSeedFormatter();
        } else if (getDatabaseURL().startsWith("jdbc:postgresql:")) {
            // Production Postgres
            result.createTableFormatter = new PostgresTableBlueprintFormatter();
            result.createRelationFormatter = new PostgresTableRelationFormatter();
            result.insertSeedsFormatter = new PostgresDatabaseSeedFormatter();
        } else if (getDatabaseURL().startsWith("jdbc:mysql:")) {
            // Production Postgres
            result.createTableFormatter = new MysqlTableBlueprintFormatter();
            result.createRelationFormatter = new MysqlTableRelationFormatter();
            result.insertSeedsFormatter = new MysqlDatabaseSeedFormatter();
        }

        return result;
    }

    private void applyMigration(IDatabasePreparationStep migration, Connection connection, SqlDialectFormatter formatters) {
        String command = null;

        if (migration instanceof IDatabaseSeed) {
            command = getMigrationSQL(migration, formatters);
            executeSQL(migration, command, connection);
        } else if (migration instanceof IMigrationWithCustomExecute && migration instanceof IDatabaseMigration) {
            command = ((IDatabaseMigration) migration).getMigrationDDL(formatters);
            ((IMigrationWithCustomExecute) migration).execute(connection);
        } else if (migration instanceof IDatabaseMigration) {
            command = ((IDatabaseMigration) migration).getMigrationDDL(formatters);
            executeSQL(migration, command, connection);
        } else {
            throw new RuntimeException();
        }

        IDatabaseMigrationEntry m = instantiateNewMigrationEntryForDB();
        m.setMigrationName(migration.name());
        m.setCommand(command);
        m.setAppliedTimestamp(Timestamp.from(Instant.now()));

        saveMigrationEntryToDatabase(m);
    }

    protected abstract IDatabaseMigrationEntry saveMigrationEntryToDatabase(IDatabaseMigrationEntry m);

    protected abstract IDatabaseMigrationEntry instantiateNewMigrationEntryForDB();

    private void executeSQL(IDatabasePreparationStep migration, String ddl, Connection connection) {
        try {
            Statement statement = connection.createStatement();
            statement.execute(ddl);
        } catch (Exception ex) {
//            logger.error("Unable to run migration: " + migration.name());
//            logger.error("Migration DDL:");
//            logger.error(ddl);
//            logger.error("Exception message: " + ex.getMessage());
            boolean shouldThrowException = true;
            if (migration instanceof IDatabaseMigration && ((IDatabaseMigration) migration).allowFailure()) {
                shouldThrowException = false;
            }

            if (shouldThrowException) {
                throw new RuntimeException(String.format("Migration '%s' failed with an exception.", migration.name()), ex);
            }
        }
    }

    private boolean hasBeenApplied(IDatabasePreparationStep migration, List appliedDatabaseMigrations) {
        return getMigrationRecord(migration, appliedDatabaseMigrations) != null;
    }

    private IDatabaseMigrationEntry getMigrationRecord(IDatabasePreparationStep migration, List appliedDatabaseMigrations) {
        IDatabaseMigrationEntry result = null;

        for (IDatabaseMigrationEntry m : appliedDatabaseMigrations) {
            if (m.getMigrationName().equals(migration.name())) {
                result = m;
                break;
            }
        }

        return result;
    }

    protected abstract List getListOfMigrations();

    private List getAllDatabasePreparationSteps() {
        Map migrations = getMigrationBeans();
        Map stepsMap = new HashMap<>();
        Map stepsMigrationMap = new HashMap<>();
        List steps = new ArrayList<>();

        List noDependencyMigrations = new ArrayList<>();
        for (String migrationName : migrations.keySet()) {
            IDatabasePreparationStep migration = migrations.get(migrationName);

            WorkflowStepInfo step = new WorkflowStepInfo();
            step.id = migration.name();
            step.name = migration.name();

            stepsMap.put(migration.name(), step);
            stepsMigrationMap.put(migration.name(), migration);
            steps.add(step);

            if (migration.getDependencies().isEmpty()) {
                noDependencyMigrations.add(migration);
            }
        }

        for (String migrationName : migrations.keySet()) {
            IDatabasePreparationStep migration = migrations.get(migrationName);
            WorkflowStepInfo step = stepsMap.get(migration.name());

            if (!migration.getDependencies().isEmpty()) {
                step.dependencies = new ArrayList<>();
                for (String depName : migration.getDependencies()) {
                    step.dependencies.add(stepsMap.get(depName));
                }
            }
        }

        if (noDependencyMigrations.size() != 1) {
            // Why this exception happens:
            //    1. There is no migration without any dependency. This is most likely an indication of a loop in the
            //       migration dependency.
            //    2. There are at least two migrations without any dependency. Considering that there should already
            //       be on that creates the database, this means the other migrations should at least be dependent of
            //       the create database migration.
            throw new RuntimeException("The migrations list should only have one migration without any dependency.");
        }

        for (String migrationName : migrations.keySet()) {
            IDatabasePreparationStep migration = migrations.get(migrationName);

            List dependencies = stepsMap.get(migration.name()).dependencies;
            for (String dependency : migration.getDependencies()) {
                dependencies.add(stepsMap.get(dependency));
            }
        }

        // Sort the DAG steps and run them 1 by 1
        WorkflowDAG graph = WorkflowDAG.from(steps);
        return graph.getSortedList().stream().map(s -> stepsMigrationMap.get(s.name)).collect(Collectors.toList());
    }

    protected abstract Map getMigrationBeans();
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy