Handling von Datenbank-Updates mit Spring

Logo von Liquibase

Aufbauend auf meinem vorherigen Artikel „Handling von Datenbank-Updates„, das die Datenbank-Änderungen in einer „Standalone“-Anwendung beschreibt, lässt sich dieses Verfahren auch leicht auf Enterprise Anwendungen ausweiten. In diesem Beispiel verwenden wir als Enterprise Framework Spring.

Zu fügen wir Spring als weitere Maven Abhängigkeit hinzu:

<!-- Spring -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
</dependency>

Wie auch schon bei der Standalone-Variante legen wir die Datenbank-Updates unter /database/changelog.properties an. Damit die Updates geführt werden, erstellen wir uns eine neue Klasse LiquibaseUpdateDatabaseService:

/**
 * Manager zur Verwaltung und Aktualisierung von Datenbank-Updates.
 * Beim Einsatz als Spring Bean wird diese Aktualisierung direkt nach der Instanzierung
 * der Bean durchgeführt.
 *
 * @author Denis Wirries
 * @version 1.0
 * @since 16.08.14
 */
public class LiquibaseUpdateDatabaseService implements InitializingBean, UpdateDatabaseService {

    private static final Logger LOG = LoggerFactory.getLogger(LiquibaseUpdateDatabaseService.class);

    @Autowired
    private DataSource dataSource;

    private String changeTasksFile = "database/changelog.properties";
    private boolean excuteOnLoad = true;
    private String[] changeTaskFiles;

    /**
     * Standard-Construktur.
     */
    public LiquibaseUpdateDatabaseService() {
        LOG.debug("Creating new instance of LiquibaseUpdateDatabaseService");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setExcuteOnLoad(final boolean excuteOnLoad) {
        this.excuteOnLoad = excuteOnLoad;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setDataSource(final DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setChangeTasks(final String changeTasksFile) {
        this.changeTasksFile = changeTasksFile;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setChangeTaskFiles(final String[] changeTaskFiles) {
        if (changeTaskFiles != null) {
            this.changeTaskFiles = Arrays.copyOf(changeTaskFiles, changeTaskFiles.length);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        if (excuteOnLoad) update();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void update() throws UpdateException {
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
            final DatabaseFactory databaseFactory = DatabaseFactory.getInstance();
            final Database database = databaseFactory.findCorrectDatabaseImplementation(new JdbcConnection(connection));

            LOG.info("Start updating database ...");
            final List<String> updateResources = loadUpdateList();
            for (final String ur : updateResources)
                doUpdate(database, ur);

            LOG.info("Update completed");

        } catch (final Exception ex) {
            throw new UpdateException("Error while updating database", ex);
        } finally {
            close(connection);
        }
    }

    /**
     * Schließt die Datenbank-Verbindung.
     *
     * @param connection Datenbank-Verbindung
     */
    private void close(final Connection connection) {
        if (connection == null) return;
        try {
            connection.close();
        } catch (final SQLException e) {
            LOG.debug("Error while closing connection", e);
        }
    }

    /**
     * Führt die Aktualisierung der Datenbank mit der angegebenen Resource für das Update-File durch.
     *
     * @param database       Datenbank
     * @param updateResource Resource mit der Datenbank.
     * @throws liquibase.exception.LiquibaseException Fehler beim Aktualisieren
     */
    private void doUpdate(final Database database, final String updateResource) throws LiquibaseException {
        LOG.info("Update database with " + updateResource);
        final Liquibase liquibase = new Liquibase(updateResource, new ClassLoaderResourceAccessor(), database);
        liquibase.update("");
    }

    /**
     * Lädt die Liste der Updates.
     *
     * @return Liste mit Update-Ressourcen
     */
    protected List<String> loadUpdateList() {
        if (changeTaskFiles != null && changeTaskFiles.length > 0) {
            return Arrays.asList(changeTaskFiles);
        }

        // Lade Properties Files
        final List<String> list = new ArrayList<String>();
        final Properties props = loadPropertiesFromClassPath(changeTasksFile);

        // Da Keys als HashMap gespeichert werden, ist die Reihenfolge nicht gewährleistet
        final List<String> keys = new ArrayList<String>();
        for (final Object key : props.keySet()) {
            keys.add((String) key);
        }
        Collections.sort(keys);

        // Schlüssel in richtiger Reihenfolge auslesen
        for (final String key : keys) {
            list.add(props.getProperty(key));
        }

        return list;
    }

}

Diese Klasse implementiert das Interface InitializingBean, das nach dem Initialisieren der Spring Dependencies, die Methode afterPropertiesSet ausführt. In dieser Methode wird dann das Update der Datenbank gestartet (analog der Standalone-Variante). Wird diese Bean also im Spring Context initialisiert, so wird auch automatisch beim Hochfahren die Datenbank auf den letzten Stand gebracht. Die mögliche Spring-Context-XML kann wie folgt aussehen:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans      http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context    http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx         http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- Spring Data Configuration -->

    <!-- Load properties from file -->
    <context:property-placeholder location="classpath*:spring/environment.properties"/>

    <!-- Setup the DataSource and Connection Pool -->
    <bean id="dataSource"
          class="org.apache.commons.dbcp.BasicDataSource"
          destroy-method="close"
          p:driverClassName="${database.driverClassName}"
          p:url="${database.url}"
          p:username="${database.username}"
          p:password="${database.password}"
          p:testOnBorrow="true"
          p:testOnReturn="true"
          p:testWhileIdle="true"
          p:timeBetweenEvictionRunsMillis="1800000"
          p:numTestsPerEvictionRun="3"
          p:minEvictableIdleTimeMillis="1800000"/>

    <!-- Setup the SessionFactory -->
    <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <!-- DataSource for SessionFactory -->
        <property name="dataSource" ref="dataSource"/>

        <!-- Settings for Hibernate -->
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">${database.dialect}</prop>
                <prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
                <!-- Caching -->
                <!--<prop key="hibernate.cache.use_second_level_cache">true</prop>-->
                <!--<prop key="hibernate.cache.use_query_cache">true</prop>-->
                <!--<prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop>-->
                <!-- Schema Validation -->
                <!-- value="create" to build a new database on each run; -->
                <!-- value="update" to modify an existing database; -->
                <!-- value="create-drop" means the same as "create" but also drops tables when Hibernate closes; -->
                <!-- value="validate" makes no changes to the database -->
                <!--<prop key="hibernate.hbm2ddl.auto">validate</prop>-->
                <!-- Debug: show and format sql -->
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.format_sql">true</prop>
            </props>
        </property>

        <!-- Entity-Classes -->
        <!--
        <property name="annotatedClasses">
            <list>
                <value>SomeEntities</value>
            </list>
        </property>
        -->
    </bean>

    <!-- Setup Transaction Manager -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <bean id="transactionManager"
          class="org.springframework.orm.hibernate4.HibernateTransactionManager"
          p:sessionFactory-ref="sessionFactory"
          p:dataSource-ref="dataSource"/>

    <!-- Setup and excute database updates -->
    <bean id="databaseUpdater"
          class="de.netpage.sample.database.update.spring.LiquibaseUpdateDatabaseService"
          p:changeTasks="database/update-database.properties"
          p:excuteOnLoad="true"/>

</beans>

PS: Ein schöner Vorteil beim Einsatz von Spring ist, das man für die verschiedenen Umgebungen (wie z.B. lokal, Tests, T- / P-Umgebung), einfach Property-Dateien anlegen kann. Diese werden mittels

<context:property-placeholder location="classpath*:spring/environment.properties"/>

dann automatisch beim Start geladen und ersetzten die Platzhalter in der Spring-Context-XML (wie z.B. ${database.url}).

Das vollständige Beispiel findet sich in meinem GitHub-Repository: https://github.com/denisw160/LiquibaseUpdate/releases/tag/0.1

1 comment for “Handling von Datenbank-Updates mit Spring

Schreibe einen Kommentar