5 июн. 2009 г.

Database population. Liquibase и DbUnit.

Очень долго не постил, хотя бредовых идей было много. Об одной сейчас расскажу.
Идея такая - захотелось поддерживать актуальность базы данных на уровне приложения, т.е. изменяем код приложения, комиттим, билдим проект и при старте обновляется БД.
До этого структура базы разрабатывалась на основе liquibase changeLog-ов и обновлялось с помощью плагина под maven. Решил, что неплохо было бы использовать этот механизм.

Как оно работает? Основной класс liquibase.Liquibase, включающий в себя основные методы для работы с БД. Необходимый мне метод - update. Для начала создадим интерфейс:

package sody.common.populator;

/**
 * @author Ivan Khalopik
 * @version $Revision: 387 $ $Date: 2009-06-08 14:58:33 +0300 (Пн, 08 июн 2009) $
 */
public interface Populator {

 void populate() throws PopulatorException;

}

package sody.common.populator;

/**
 * @author Ivan Khalopik
 * @version $Revision: 387 $ $Date: 2009-06-08 14:58:33 +0300 (Пн, 08 июн 2009) $
 */
public class PopulatorException extends Exception {
 public PopulatorException() {
 }

 public PopulatorException(final String message) {
  super(message);
 }

 public PopulatorException(final String message, final Throwable cause) {
  super(message, cause);
 }

 public PopulatorException(final Throwable cause) {
  super(cause);
 }
}

Интерфейс нам понадобится в случае, если решим использовать что-то отличное от liquibase для популяции БД. Далее подключаем зависимость:

<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
    <version>1.9.1</version>
</dependency>

package sody.common.populator;

import liquibase.FileOpener;
import liquibase.Liquibase;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.Properties;
import java.util.Vector;

/**
 * @author Ivan Khalopik
 * @version $Revision: 387 $ $Date: 2009-06-08 14:58:33 +0300 (Пн, 08 июн 2009) $
 */
public class LiquibasePopulator implements Populator, ResourceLoaderAware {

 private ResourceLoader resourceLoader;
 private DataSource dataSource;
 private String schema;
 private String changeLogFile;
 private String contexts;
 private boolean skipPopulate;
 public static final String SCHEMA_PROPERTY = "liquibase.schema";
 private static final String CHANGE_LOG_FILE_PROPERTY = "liquibase.changeLogFile";
 public static final String SKIP_POPULATE_PROPERTY = "liquibase.skipPopulate";

 public void populate() throws PopulatorException {
  if (!skipPopulate) {
   Connection connection = null;
   try {
    connection = dataSource.getConnection();
    final Liquibase liquibase = new Liquibase(changeLogFile, new SpringResourceOpener(changeLogFile), connection);
    liquibase.getDatabase().setDefaultSchemaName(schema);
    liquibase.update(contexts);
   } catch (Exception e) {
    throw new PopulatorException("could not populate database", e);
   } finally {
    if (connection != null) {
     try {
      connection.rollback();
      connection.close();
     } catch (SQLException e) {
      //nothing to do
     }
    }
   }
  }
 }

 public void setResourceLoader(final ResourceLoader resourceLoader) {
  this.resourceLoader = resourceLoader;
 }

 public void setProperties(final Properties properties) {
  final String schema = properties.getProperty(SCHEMA_PROPERTY);
  final String changeLogFile = properties.getProperty(CHANGE_LOG_FILE_PROPERTY);
  final String skipPopulate = properties.getProperty(SKIP_POPULATE_PROPERTY);

  setSchema(schema);
  setChangeLogFile(changeLogFile);
  setSkipPopulate(Boolean.valueOf(skipPopulate));
 }

 public void setDataSource(final DataSource dataSource) {
  this.dataSource = dataSource;
 }

 public void setChangeLogFile(final String changeLogFile) {
  this.changeLogFile = changeLogFile;
 }

 public void setContexts(final String contexts) {
  this.contexts = contexts;
 }

 public void setSkipPopulate(final boolean skipPopulate) {
  this.skipPopulate = skipPopulate;
 }

 public void setSchema(final String schema) {
  this.schema = schema;
 }

 class SpringResourceOpener implements FileOpener {
  private String parentFile;

  public SpringResourceOpener(String parentFile) {
   this.parentFile = parentFile;
  }

  public InputStream getResourceAsStream(final String file) throws IOException {
   return getResource(file).getInputStream();
  }

  public Enumeration getResources(String packageName) throws IOException {
   Vector tmp = new Vector();
   tmp.add(getResource(packageName).getURL());
   return tmp.elements();
  }

  public Resource getResource(String file) {
   return resourceLoader.getResource(adjustClasspath(file));
  }

  private String adjustClasspath(String file) {
   return isClasspathPrefixPresent(parentFile) && !isClasspathPrefixPresent(file)
     ? ResourceLoader.CLASSPATH_URL_PREFIX + file
     : file;
  }

  public boolean isClasspathPrefixPresent(String file) {
   return file.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX);
  }

  public ClassLoader toClassLoader() {
   return resourceLoader.getClassLoader();
  }
 }
}

В итоге получили популятор, который при вызове метода populate читает changeLogFile и в соответствие с ним обновляет БД. Параметры можно задавать либо напрямую, либо через Properties
dataSource - DataSource нашей БД
schema - схема по умолчанию
changeLogFile - понятно
skipPopulate - отключить обновление БД

Кроме структуры, иногда нужно поддерживать актуальность содержимого БД. Это конечно можно делать с помощью того же liquibase, но это подходит только для статических, не изменяющихся данных. В редких случаях нам нужны некоторые тестовые данные, для тестового запуска приложения. Для таких случаев я решил использовать DbUnit. Все что нужно - написать еще одну реализацию нашего популятора:

package sody.common.populator;

import org.dbunit.DataSourceDatabaseTester;
import org.dbunit.IDatabaseTester;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;

import javax.sql.DataSource;
import java.util.Properties;

/**
 * @author Ivan Khalopik
 * @version $Revision: 387 $ $Date: 2009-06-08 14:58:33 +0300 (Пн, 08 июн 2009) $
 */
public class DbUnitPopulator implements Populator, ResourceLoaderAware {

 private ResourceLoader resourceLoader;
 private DataSource dataSource;
 private String schema;
 private String dataSetFile;
 private boolean skipPopulate;
 private DatabaseOperation setUpOperation = DatabaseOperation.REFRESH;
 private DatabaseOperation tearDownOperation = DatabaseOperation.NONE;

 public void populate() throws PopulatorException {
  if (!skipPopulate) {
   IDatabaseTester databaseTester = new DataSourceDatabaseTester(dataSource);
   databaseTester.setSchema(schema);
   databaseTester.setSetUpOperation(setUpOperation);
   databaseTester.setTearDownOperation(tearDownOperation);
   final Resource dataSet = resourceLoader.getResource(dataSetFile);
   try {
    databaseTester.setDataSet(new FlatXmlDataSet(dataSet.getFile()));
    databaseTester.onSetup();
   } catch (Exception e) {
    throw new PopulatorException("can not populate database", e);
   }
  }
 }

 public void setProperties(final Properties properties) {
  final String schema = properties.getProperty("dbunit.schema");
  final String dataSetFile = properties.getProperty("dbunit.dataSetFile");
  final String skip = properties.getProperty("dbunit.skipPopulate");

  setSchema(schema);
  setDataSetFile(dataSetFile);
  setSkipPopulate(Boolean.valueOf(skip));
 }

 public void setSetUpOperation(final DatabaseOperation setUpOperation) {
  this.setUpOperation = setUpOperation;
 }

 public void setTearDownOperation(final DatabaseOperation tearDownOperation) {
  this.tearDownOperation = tearDownOperation;
 }

 public void setDataSource(final DataSource dataSource) {
  this.dataSource = dataSource;
 }

 public void setDataSetFile(final String dataSetFile) {
  this.dataSetFile = dataSetFile;
 }

 public void setSkipPopulate(final boolean skipPopulate) {
  this.skipPopulate = skipPopulate;
 }

 public void setSchema(final String schema) {
  this.schema = schema;
 }

 public void setResourceLoader(final ResourceLoader resourceLoader) {
  this.resourceLoader = resourceLoader;
 }
}

Получаем популятор, который при вызове метода populate читает dateSetFile и в соответствие с ним обновляет БД. Параметры как и в предыдущем примере можно задавать либо напрямую, либо через Properties
dataSource - DataSource нашей БД
schema - схема по умолчанию
dataSetFile - понятно что
skipPopulate - запретить популяцию

Осталось дело за малым - заставить популяторы запускаться со стартом приложения. Для этого создадим небольшой классик:

package sody.common.populator;

import org.springframework.beans.factory.InitializingBean;

/**
 * @author Ivan Khalopik
 * @version $Revision: 387 $ $Date: 2009-06-08 14:58:33 +0300 (Пн, 08 июн 2009) $
 */
public class SpringInitializingPopulatorBean implements InitializingBean {
 private final Populator populator;

 public SpringInitializingPopulatorBean(final Populator populator) {
  this.populator = populator;
 }

 public void afterPropertiesSet() throws Exception {
  populator.populate();
 }
}

... и проделать следующее в applicationContext.xml

<bean id="liquibasePopulator" class="sody.common.populator.SpringInitializingPopulatorBean">
  <constructor-arg>
   <bean class="sody.common.populator.LiquibasePopulator"
      p:dataSource-ref="dataSource"
      p:properties-ref="jdbcProperties"
     />
  </constructor-arg>
 </bean>


 <bean id="dbUnitPopulator" class="sody.common.populator.SpringInitializingPopulatorBean"
    depends-on="liquibasePopulator">
  <constructor-arg>
   <bean class="sody.common.populator.DbUnitPopulator" depends-on="liquibasePopulator"
      p:dataSource-ref="dataSource"
      p:properties-ref="jdbcProperties"
     />
  </constructor-arg>
 </bean>

И если у нас есть бины, которые должны создаваться после популяции БД, то:

<bean id="hibernateTemplate" class="sody.common.domain.hibernate.HibernateTemplateEx" depends-on="dbUnitPopulator"
    p:sessionFactory-ref="sessionFactory"
    p:filterName="existing"/>

 <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" depends-on="dbUnitPopulator"
    p:dataSource-ref="dataSource"/>

Комментариев нет: