Очень долго не постил, хотя бредовых идей было много. Об одной сейчас расскажу.
Идея такая - захотелось поддерживать актуальность базы данных на уровне приложения, т.е. изменяем код приложения, комиттим, билдим проект и при старте обновляется БД.
Идея такая - захотелось поддерживать актуальность базы данных на уровне приложения, т.е. изменяем код приложения, комиттим, билдим проект и при старте обновляется БД.
До этого структура базы разрабатывалась на основе 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"/>
Комментариев нет:
Отправить комментарий