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

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

10 апр. 2009 г.

Hibernate. Интернационализация БД. О том как плохо получилось.

Снова пятница (не тяпница, по тяпницам у меня хорошее настроение), на улице дождь, настроение в ноль. Целую неделю прихожу на работу на час раньше, ну не дурень разве?)
В добавок ко всему больной... Ну да ладно о плохом, раскажу ка я лучше о своей очередной бредовой идее.
Проснулся как то ночью и решил, хочу чтобы мои справочники в БД были локализованы... Начал рыться по нету, что да как... Суть в том, что есть какие то сущности замапленые с помощью hibernate на реляционную БД, у этих сущностей есть нейкий description, который и нужно локализировать.

Так вот, подумал немного и кое как с горем попалам придумал кое чего, хотя мне кажется, что уж слишком грязно все получилось. Ну да ладно, как бы не было стыдно, покажу то, что у меня получилось.
Для начала сделал вот такой интерфейсик, для интернационализации чего-то, и 2 его реализации
package itdep.common.i18n;

import java.util.Locale;

/**
* @author Ivan Khalopik
* @version $Revision: 267 $ $Date: 2009-04-09 09:38:38 +0300 (Чт, 09 апр 2009) $
*/
public interface I18n<T> {

  T getValue(final Locale locale);

  void setValue(final Locale locale, final T value);

}

package itdep.common.i18n.impl;

import itdep.common.i18n.I18n;

import java.util.Locale;

/**
 * @author Ivan Khalopik
 * @version $Revision: 267 $ $Date: 2009-04-09 09:38:38 +0300 (Чт, 09 апр 2009) $
 */
public class SimpleI18n<T> implements I18n<T> {
  private T value;

  public SimpleI18n() {
  }

  public SimpleI18n(final T value) {
    this.value = value;
  }

  public T getValue(final Locale locale) {
    return value;
  }

  public void setValue(final Locale locale, final T value) {
    this.value = value;
  }

  @Override
  public String toString() {
    return String.valueOf(value);
  }
}

package itdep.common.i18n.impl;

import itdep.common.i18n.I18n;
import itdep.common.util.I18nUtils;

import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * @author Ivan Khalopik
 * @version $Revision: 268 $ $Date: 2009-04-09 11:55:04 +0300 (Чт, 09 апр 2009) $
 */
public class I18nMapWrapper<T> implements I18n<T> {
  private final Map<Locale, T> values;

  public I18nMapWrapper(final Map<Locale, T> values) {
    this.values = values;
  }

  public T getValue(final Locale locale) {
    final List<Locale> candidates = I18nUtils.getCandidateLocales(locale);
    for (Locale candidate : candidates) {
      if (values.containsKey(candidate)) {
        return values.get(candidate);
      }
    }
    return null;
  }

  public void setValue(final Locale locale, final T value) {
    values.put(locale, value);
  }

  @Override
  public String toString() {
    return String.valueOf(getValue(Locale.getDefault()));
  }
}

Первая реализация - это что вроде заглушки, когда объект во всех локалях имеет одно и то же значение, во вторую подсовываем мапу с ключем в виде локали, из которой в соответсвии с нужной локалью и будет браться локализированое значение.
Далее все это нужно совместить с hibernate сущностью. Для этого объявил интерфейс:<>
package itdep.common.i18n;

import itdep.common.dao.Entity;

import java.io.Serializable;

/**
 * @author Ivan Khalopik
 * @version $Revision: 265 $ $Date: 2009-04-08 11:37:12 +0300 (Ср, 08 апр 2009) $
 */
public interface I15dEntity<PK extends Serializable> extends Entity<PK> {

  I18n<String> getDescription();

}

Ну и реализация
package itdep.model;

import itdep.common.i18n.I18n;
import itdep.common.i18n.impl.I18nMapWrapper;
import org.hibernate.annotations.CollectionOfElements;
import org.hibernate.annotations.MapKey;

import javax.persistence.*;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

/**
 * @author Ivan Khalopik
 * @version $Revision: 265 $ $Date: 2009-04-08 11:37:12 +0300 (Ср, 08 апр 2009) $
 */
@MappedSuperclass
public abstract class CodedEntity extends SomeEntity implements I15dEntity<Long> {

  @Column(name = "CODE", nullable = true, insertable = true, updatable = true)
  private String code;

  @CollectionOfElements(fetch = FetchType.EAGER)
  @JoinTable(name = "I18N",
    joinColumns = @JoinColumn(name = "CODE", referencedColumnName = "CODE", nullable = false))
  @MapKey(columns = @Column(name = "LOCALE", nullable = true, insertable = true, updatable = true))
  @Column(name = "VALUE", nullable = false, insertable = true, updatable = true)
  private Map<Locale, String> i15dName = new HashMap<Locale, String>();

  public String getCode() {
    return code;
  }

  public void setCode(final String code) {
    this.code = code;
  }

  @Override
  public I18n<String> getDescription() {
    return new I18nMapWrapper<String>(i15dName);
  }
}

Здесь для локализации используется таблица I18N с полями CODE(код), LOCALE(локаль), VALUE(значение), в самой таблице сущности храним поле CODE, которое и соотетсвует тому, что в I18N. В итоге получаем, что #getDescription() возвращает нам локализированую строку. Ну а в случае, если надо, чтоб возвращалось одно и то же значение во всех локалях можно сделать так:
package itdep.model;

import itdep.common.i18n.I18n;
import itdep.common.i18n.impl.SimpleI18n;

import javax.persistence.Column;
import javax.persistence.MappedSuperclass;

/**
 * @author Ivan Khalopik
 * @version $Revision: 265 $ $Date: 2009-04-08 11:37:12 +0300 (Ср, 08 апр 2009) $
 */
@MappedSuperclass
public abstract class NamedEntity extends SomeEntity implements I15dEntity<Long> {

  @SuppressWarnings({"UnusedDeclaration"})
  @Column(name = "NAME", nullable = false, insertable = true, updatable = true)
  private String name;

  @Override
  public I18n<String> getDescription() {
    return new SimpleI18n<String>(name);
  }
}

Ну вот что-то и получилось, но мне не нравиться, буду видимо переделывать, хочу реализовать отображение через спринговый MessageSource, а редактирование можно и так, немного переделать, или, возможно, что-то совсем по другому сделать. Вобщем продолжение следует.
Все, пойду лечиться...