Показаны сообщения с ярлыком dbunit. Показать все сообщения
Показаны сообщения с ярлыком dbunit. Показать все сообщения

19 мар. 2010 г.

Java. Загрузка данных в БД в XML формате.

В эту тяпницу зародилась у меня очередная бредовая идея. Очень часто в веб проектах возникает необходимость добавить возможность загрузки данных в БД через веб интерфейс. Проще всего сделать это путем загрузки на сервер какого-то sql файла. Или в моем случае это будет XML файл в формате DbUnit.


Итак, для начала надо понять как работает этот самый DbUnit. Есть такой интерфейс IDatabaseTester, который конфигуряется настройками соединения с БД, setUp и tearDown операциями. Ему передается объект типа IDataSet после чего можно вызывать метод onSetup() - выполнится setup операция, onTearDown() - tearDown операция.



Удобнее всего для решения поставленной задачи создать на стороне back-end-а сервис, который будет получать в качестве параметра некий InputStream и результатом работы которого будет обновленная (или нет) БД. Вот Мое решение:




// PopulationService.java
package ga.common.domain;

import java.io.InputStream;

/**
* @author Ivan Khalopik
* @version $Revision: 21 $ $Date: 2010-03-17 11:18:19 +0200 (Ср, 17 мар 2010) $
*/
public interface PopulationService {

void populate(InputStream inputStream);

}





// DbUnitPopulationService.java
package ga.common.domain;

import org.dbunit.IDatabaseTester;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;

import java.io.InputStream;

/**
* @author Ivan Khalopik
* @version $Revision: 21 $ $Date: 2010-03-17 11:18:19 +0200 (Ср, 17 мар 2010) $
*/
public class DbUnitPopulationService implements PopulationService {
private final IDatabaseTester databaseTester;
private final FlatXmlDataSetBuilder builder;

public DbUnitPopulationService(IDatabaseTester databaseTester) {
this.databaseTester = databaseTester;
builder = new FlatXmlDataSetBuilder();
}

public void populate(InputStream inputStream) {
try {
final IDataSet dataSet = builder.build(inputStream);
databaseTester.setDataSet(dataSet);
databaseTester.onSetup();
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
databaseTester.setDataSet(null);
}
}
}



Внутри сервиса на основе InputStream-а создается FlatXmlDataSet (ну или можно любой другой по желанию) и вызывается метод onSetup(). Можно пойти еще дальше и в секции catch выполнить метод onTearDown(). Однако для работы такого сервиса необходимо еще создать объект IDatabaseTester. Я предлагаю это сделать с помощью спрингового FactoryBean-а:




// DatabaseTesterFactoryBean.java
package ga.common.dbunit;

import org.dbunit.DataSourceDatabaseTester;
import org.dbunit.IDatabaseTester;
import org.dbunit.operation.DatabaseOperation;
import org.springframework.beans.factory.config.AbstractFactoryBean;

import javax.sql.DataSource;

/**
* @author Ivan Khalopik
* @version $Revision: 21 $ $Date: 2010-03-17 11:18:19 +0200 (Ср, 17 мар 2010) $
*/
public class DatabaseTesterFactoryBean extends AbstractFactoryBean {
private DataSource dataSource;

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

@Override
public Class getObjectType() {
return IDatabaseTester.class;
}

@Override
protected IDatabaseTester createInstance() throws Exception {
final IDatabaseTester tester = new DataSourceDatabaseTester(dataSource);
tester.setSetUpOperation(DatabaseOperation.REFRESH);
tester.setTearDownOperation(DatabaseOperation.NONE);
return tester;
}
}



Забыл сказать, что операции бывают следующие:
• NONE - ничего не делать
• UPDATE - апдейтнуть данные
• INSERT - заинсертить данные
• REFRESH - заинсертить те, которых не хватает, апдейтнуть те, которые уже есть
• DELETE - удалить данные
• DELETE_ALL - очистить БД
• CLEAN_INSERT то же, что DELETE_ALL, а затем INSERT
Я использовал здесь REFRESH на сетапе, так как в данном случае больше всего подходит.


Итак, мы имеем сервис умеющий загружать данные в БД, теперь его можно применять на UI или в сервисах, например для автоинициализации БД при старте приложения. Например код страницы на Tapestry 5 будет выглядеть так:




// Populate.java
package ga.web.pages.security;

import ga.common.domain.PopulationService;
import ga.web.base.pages.AbstractPage;
import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.upload.services.UploadedFile;

/**
* @author Ivan Khalopik
* @version $Revision: 22 $ $Date: 2010-03-17 11:19:53 +0200 (Ср, 17 мар 2010) $
*/
public class Populate extends AbstractPage {

@Inject
private PopulationService populationService;

private UploadedFile file;

@Component
private Form populateForm;

public UploadedFile getFile() {
return file;
}

public void setFile(UploadedFile file) {
this.file = file;
}

public void onPopulate() {
if (populateForm.isValid()) {
populationService.populate(file.getStream());
}
}
}



Полный вариант исходников можно найти здесь


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"/>