15
votes

I'm trying to inject component of configuration properties in the flyway migration java code but it always null.

I'm using spring boot with Flyway.

@Component
@ConfigurationProperties(prefix = "code")
public class CodesProp {

    private String codePath;
 }

Then inside Flyway migration code, trying to autowrire this component as following:

public class V1_4__Migrate_codes_metadata implements SpringJdbcMigration {

@Autowired
private CodesProp codesProp ;
public void migrate(JdbcTemplate jdbcTemplate) throws Exception {
    codesProp.getCodePath();  
}

Here, codesProp is always null.

Is there any way to inject spring beans inside flyway or make it initialized before flyway bean?

Thank You.

4

4 Answers

6
votes

Flyway doesn't support dependency injection into SpringJdbcMigration implementations. It simply looks for classes on the classpath that implement SpringJdbcMigration and creates a new instance using the default constructor. This is performed in SpringJdbcMigrationResolver. When the migration is executed, SpringJdbcMigrationExecutor creates a new JdbcTemplate and then calls your migration implementation's migrate method.

If you really need dependencies to be injected into your Java-based migrations, I think you'll have to implement your own MigrationResolver that retrieves beans of a particular type from the application context and creates and returns a ResolvedMigration instance for each.

6
votes

If like me, you don't want to wait for Flyway 4.1, you can use Flyway 4.0 and add the following to your Spring Boot application:

1) Create a ApplicationContextAwareSpringJdbcMigrationResolver class in your project:

import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.api.MigrationType;
import org.flywaydb.core.api.MigrationVersion;
import org.flywaydb.core.api.configuration.FlywayConfiguration;
import org.flywaydb.core.api.migration.MigrationChecksumProvider;
import org.flywaydb.core.api.migration.MigrationInfoProvider;
import org.flywaydb.core.api.migration.spring.SpringJdbcMigration;
import org.flywaydb.core.api.resolver.ResolvedMigration;
import org.flywaydb.core.internal.resolver.MigrationInfoHelper;
import org.flywaydb.core.internal.resolver.ResolvedMigrationComparator;
import org.flywaydb.core.internal.resolver.ResolvedMigrationImpl;
import org.flywaydb.core.internal.resolver.spring.SpringJdbcMigrationExecutor;
import org.flywaydb.core.internal.resolver.spring.SpringJdbcMigrationResolver;
import org.flywaydb.core.internal.util.ClassUtils;
import org.flywaydb.core.internal.util.Location;
import org.flywaydb.core.internal.util.Pair;
import org.flywaydb.core.internal.util.StringUtils;
import org.flywaydb.core.internal.util.scanner.Scanner;
import org.springframework.context.ApplicationContext;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;

/**
 * Migration resolver for {@link SpringJdbcMigration}s which are registered in the given {@link ApplicationContext}.
 * This resolver provides the ability to use other beans registered in the {@link ApplicationContext} and reference
 * them via Spring's dependency injection facility inside the {@link SpringJdbcMigration}s.
 */
public class ApplicationContextAwareSpringJdbcMigrationResolver extends SpringJdbcMigrationResolver {

    private final ApplicationContext applicationContext;

    public ApplicationContextAwareSpringJdbcMigrationResolver(Scanner scanner, Location location, FlywayConfiguration configuration, ApplicationContext applicationContext) {
        super(scanner, location, configuration);
        this.applicationContext = applicationContext;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Collection<ResolvedMigration> resolveMigrations() {
        // get all beans of type SpringJdbcMigration from the application context
        Map<String, SpringJdbcMigration> springJdbcMigrationBeans =
                (Map<String, SpringJdbcMigration>) this.applicationContext.getBeansOfType(SpringJdbcMigration.class);

        ArrayList<ResolvedMigration> resolvedMigrations = new ArrayList<ResolvedMigration>();

        // resolve the migration and populate it with the migration info
        for (SpringJdbcMigration springJdbcMigrationBean : springJdbcMigrationBeans.values()) {
            ResolvedMigrationImpl resolvedMigration = extractMigrationInfo(springJdbcMigrationBean);
            resolvedMigration.setPhysicalLocation(ClassUtils.getLocationOnDisk(springJdbcMigrationBean.getClass()));
            resolvedMigration.setExecutor(new SpringJdbcMigrationExecutor(springJdbcMigrationBean));

            resolvedMigrations.add(resolvedMigration);
        }

        Collections.sort(resolvedMigrations, new ResolvedMigrationComparator());
        return resolvedMigrations;
    }

    ResolvedMigrationImpl extractMigrationInfo(SpringJdbcMigration springJdbcMigration) {
        Integer checksum = null;
        if (springJdbcMigration instanceof MigrationChecksumProvider) {
            MigrationChecksumProvider version = (MigrationChecksumProvider) springJdbcMigration;
            checksum = version.getChecksum();
        }

        String description;
        MigrationVersion version1;
        if (springJdbcMigration instanceof MigrationInfoProvider) {
            MigrationInfoProvider resolvedMigration = (MigrationInfoProvider) springJdbcMigration;
            version1 = resolvedMigration.getVersion();
            description = resolvedMigration.getDescription();
            if (!StringUtils.hasText(description)) {
                throw new FlywayException("Missing description for migration " + version1);
            }
        } else {
            String resolvedMigration1 = ClassUtils.getShortName(springJdbcMigration.getClass());
            if (!resolvedMigration1.startsWith("V") && !resolvedMigration1.startsWith("R")) {
                throw new FlywayException("Invalid Jdbc migration class name: " + springJdbcMigration.getClass()
                                                                                                     .getName() + " => ensure it starts with V or R," + " or implement org.flywaydb.core.api.migration.MigrationInfoProvider for non-default naming");
            }

            String prefix = resolvedMigration1.substring(0, 1);
            Pair info = MigrationInfoHelper.extractVersionAndDescription(resolvedMigration1, prefix, "__", "");
            version1 = (MigrationVersion) info.getLeft();
            description = (String) info.getRight();
        }

        ResolvedMigrationImpl resolvedMigration2 = new ResolvedMigrationImpl();
        resolvedMigration2.setVersion(version1);
        resolvedMigration2.setDescription(description);
        resolvedMigration2.setScript(springJdbcMigration.getClass().getName());
        resolvedMigration2.setChecksum(checksum);
        resolvedMigration2.setType(MigrationType.SPRING_JDBC);
        return resolvedMigration2;
    }
}

2) Add a new configuration class to post process the Spring Boot generated Flyway instance:

import org.flywaydb.core.Flyway;
import org.flywaydb.core.internal.dbsupport.DbSupport;
import org.flywaydb.core.internal.dbsupport.h2.H2DbSupport;
import org.flywaydb.core.internal.dbsupport.mysql.MySQLDbSupport;
import com.pegusapps.zebra.infrastructure.repository.flyway.ApplicationContextAwareSpringJdbcMigrationResolver;
import org.flywaydb.core.internal.resolver.sql.SqlMigrationResolver;
import org.flywaydb.core.internal.util.Location;
import org.flywaydb.core.internal.util.PlaceholderReplacer;
import org.flywaydb.core.internal.util.scanner.Scanner;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.sql.SQLException;

@Configuration
@ComponentScan("db.migration")
public class FlywayConfiguration {

    @Bean
    public BeanPostProcessor postProcessFlyway(ApplicationContext context) {
        return new BeanPostProcessor() {

            @Override
            public Object postProcessBeforeInitialization(Object o, String s) throws BeansException {
                return o;
            }

            @Override
            public Object postProcessAfterInitialization(Object o, String s) throws BeansException {
                if (o instanceof Flyway) {
                    Flyway flyway = (Flyway) o;
                    flyway.setSkipDefaultResolvers(true);
                    ApplicationContextAwareSpringJdbcMigrationResolver resolver = new ApplicationContextAwareSpringJdbcMigrationResolver(
                            new Scanner(Thread.currentThread().getContextClassLoader()),
                            new Location("classpath:db/migration"),
                            context.getBean(org.flywaydb.core.api.configuration.FlywayConfiguration.class),
                            context);
                    SqlMigrationResolver sqlMigrationResolver = null;
                    try {
                        sqlMigrationResolver = new SqlMigrationResolver(
                                getDbSupport(),
                                new Scanner(Thread.currentThread().getContextClassLoader()),
                                new Location("classpath:db/migration"),
                                PlaceholderReplacer.NO_PLACEHOLDERS,
                                "UTF-8",
                                "V",
                                "R",
                                "__",
                                ".sql");
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                    flyway.setResolvers(sqlMigrationResolver, resolver);
                }
                return o;
            }

            private DbSupport getDbSupport() throws SQLException {
                DataSource dataSource = context.getBean(DataSource.class);
                if( ((org.apache.tomcat.jdbc.pool.DataSource)dataSource).getDriverClassName().equals("org.h2.Driver"))
                {
                    return new H2DbSupport(dataSource.getConnection());
                }
                else
                {
                    return new MySQLDbSupport(dataSource.getConnection());
                }
            }
        };
    }
}

Note that I have some hardcoded dependencies on tomcat jdbc pool, h2 and mysql. If you are using something else, you will need to change the code there (If there is anybody that knows how to avoid it, please comment!)

Also note that the @ComponentScan package needs to match with where you will put the Java migration classes.

Also note that I had to add the SqlMigrationResolver back in since I want to support both the SQL and the Java flavor of the migrations.

3) Create a Java class in the db.migrations package that does the actual migration:

@Component
public class V2__add_default_surveys implements SpringJdbcMigration {

    private final SurveyRepository surveyRepository;

    @Autowired
    public V2__add_surveys(SurveyRepository surveyRepository) {
        this.surveyRepository = surveyRepository;
    }

    @Override
    public void migrate(JdbcTemplate jdbcTemplate) throws Exception {
        surveyRepository.save(...);
    }
}

Note that you need to make the class a @Component and it needs to implement the SpringJdbcMigration. In this class, you can use Spring constructor injection for any Spring bean from your context you might need to do the migration(s).

Note: Be sure to disable ddl validation of Hibernate, because the validation seems to run before Flyway runs:

spring.jpa.hibernate.ddl-auto=none
1
votes

In short do not autowire beans in your db migrations or even reference classes from your application! If you refactor/delete/change classes you referenced in the migration it may not even compile or worse corrupt your migrations.

The overhead of using plain JDBC template for the migrations is not worth the risk.

0
votes

If you are using deltaspike you can use BeanProvider to get a reference to your Class. Here is a DAO example, but it should work fine with your class too.

Change your DAO code:

public static UserDao getInstance() {
    return BeanProvider.getContextualReference(UserDao.class, false, new DaoLiteral());
}

Then in your migration method:

UserDao userdao = UserDao.getInstance();

And there you've got your reference.

(referenced from: Flyway Migration with java)