DBUnit으로 이기종 간 데이터 이관하기
이전 포스팅에 이어,
현 회사에서는 이기종간 mapper dir을 별도로 관리하고있는데,
mapper는 분류하여 작업한다 하더라도,
실제 데이터( mock data )에 대해서는 해결할 방안을 찾던 중
https://woowabros.github.io/experience/2019/11/06/db-unit.html
스프링부트에서 DbUnit 을 이용하여 DB 테스트 해보기 - 우아한형제들 기술 블로그
안녕하세요. 상품시스템팀 권순규입니다. 저희 팀에서 DB 테스트를 위해 사용하고 있는 DbUnit 의 설정 및 사용에 대해 알려드리고자 합니다.
woowabros.github.io
위 포스팅을 보고 데이터 이관을 DBUnit으로 할 수 있을 것으로 보여 시작하였다.
우선 DBUnit이란 xml 형태로 dataset을 가지고오는 형태이며,
tablename이라는 table이 있고,
column / column2 의 컬럼이 있을때 아래와 같이 조회되는 형태이다.
행은 DBUnit으로 조회하는 쿼리에 row 별로 뽑힌다.
<dataset>
<tablename column="1" column2="ㅁ">
<tablename column="2" column2="ㄴ">
<tablename column="3" column2="ㅇ">
<tablename column="4" column2="ㄹ">
<tablename column="5" column2="ㅎ">
<tablename column="6" column2="ㅗ">
</dataset>
이를 이용하여,
Oracle DB의 data만 dbunit으로 빼오고, 이를 다시 dbunit insert로 이기종별 insert가 가능할 것으로 판단되 실행하였다.
하지만 DBunit에서 export -> import 형식으로 생각한 개발자가 별로 없는지 내역이 많이 나오지 않았고,
실제 export 시에도, FlatXmlDataSet 처리 간 별도로 진행해야하는 사항이 있어 디컴파일하여 처리하는것이 있었다.
우선 Oracle 기준으로 DATA 추출 코드이다.
public class ExportDBUnit {
public static void main(String[] args) throws Exception {
String outputFilePath = "C:\\solution\\gitWorkSpace\\standard-common\\src\\main\\test\\exportDBUnit";
Class driverClass = Class.forName("oracle.jdbc.driver.OracleDriver");
String user = "dbuser";
ResultSet rs = null;
Connection jdbcConnection = DriverManager.getConnection("jdbc:oracle:thin:@url:1521:sid", user, "pwd");
Statement stmt = null;
stmt = jdbcConnection.createStatement();
String sql = "SELECT UT.OBJECT_NAME AS TABLE_NAME\n" +
" FROM ALL_OBJECTS UT,\n" +
" ALL_TAB_COMMENTS UTC\n" +
" WHERE UT.OBJECT_NAME = UTC.TABLE_NAME\n" +
" AND UT.OWNER = UTC.OWNER\n" +
" AND UT.OBJECT_TYPE IN ('TABLE')\n" +
" AND UT.OWNER ='"+user.toUpperCase()+"'\n" +
"\n" +
" AND UT.OBJECT_NAME NOT LIKE 'BIN$%'\n" +
" ORDER BY TABLE_NAME";
rs = stmt.executeQuery(sql);
while(rs.next()){
IDatabaseConnection connection = new DatabaseConnection(jdbcConnection);
DatabaseConfig config = connection.getConfig();
config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new Oracle10DataTypeFactory());
config.setFeature(DatabaseConfig.FEATURE_SKIP_ORACLE_RECYCLEBIN_TABLES,Boolean.TRUE);
QueryDataSet partialDataSet = new QueryDataSet(connection);
String tableName = rs.getString("TABLE_NAME");
partialDataSet.addTable(tableName);
System.out.println("TABLE NAME :"+tableName);
/*String originalStr = partialDataSet.getTable(tableName).getValue(0,partialDataSet.getTableMetaData(tableName).getColumns()[0].getColumnName()).toString(); // 테스트
String [] charSet = {"utf-8","euc-kr","ksc5601","iso-8859-1","x-windows-949"};
for (int i=0; i<charSet.length; i++) {
for (int j=0; j<charSet.length; j++) {
try {
//FlatXmlDataSet.write(partialDataSet, new FileWriter(outputFilePath+ File.separator + tableName+".xml",false) ,"MS949" );
FileWriter t = new FileWriter(outputFilePath+ File.separator + tableName+".xml",true);
t.write( new String(originalStr.getBytes(charSet[i]), charSet[j]));
t.flush();
System.out.println("[" + charSet[i] +"," + charSet[j] +"] = " + new String(originalStr.getBytes(charSet[i]), charSet[j]));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}*/
FlatXmlDataSet.write(partialDataSet, new FileWriter(outputFilePath+ File.separator + tableName+".xml",false) ,"UTF-8" );
}
System.out.println("END");
}
이후 다른 이기종 Import 시
public class ImportDBUnit {
@Test
public void loadData() throws Exception {
String outputFilePath = "C:\\solution\\gitWorkSpace\\standard-common\\src\\main\\test\\exportDBUnit";
Class driverClass = Class.forName("oracle.jdbc.driver.OracleDriver");
String user = "user";
Connection jdbcConnection = DriverManager.getConnection("jdbc:oracle:thin:@url:1521:sid", user, "pwd");
IDatabaseConnection connection = new DatabaseConnection(jdbcConnection,user.toUpperCase());
DatabaseConfig config = connection.getConfig();
config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new Oracle10DataTypeFactory());
config.setFeature(DatabaseConfig.FEATURE_SKIP_ORACLE_RECYCLEBIN_TABLES, Boolean.TRUE);
//table 존재여부 검출용도
IDataSet databaseDataSet = connection.createDataSet();
String[] getTableNames =databaseDataSet.getTableNames();
List<String> importTableNames = new ArrayList<String>();
try {
this.deleteDBData();
for (File info : FileUtils.listFilesAndDirs(new File(outputFilePath), TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE)) {
if(info.isDirectory()) {
ArrayList<File> fileList = new ArrayList<File>();
this.arrayDirInFileList(fileList, info);
for (File inDirIsFile : fileList) {
String tableName = FilenameUtils.getBaseName(inDirIsFile.getName());
if(importTableNames.contains(tableName)) continue; //이미 import된 xml이라면 다음으로.
System.out.println("START FILE READ TABLE NAME :"+tableName);
for(String getTableName : getTableNames){
if(getTableName.equals(tableName)){
/* String[] depRootTableNames = TablesDependencyHelper.getAllDependentTables( connection, getTableName );
//상위 테이블 검색
if(depRootTableNames.length > 1){
*//*String[] depTableNames = new String[depRootTableNames.length];
this.getDependencyTableNames(connection,depRootTableNames);*//*
for(int i=depRootTableNames.length-1; i>=0; i--){
String depTableName = depRootTableNames[i];
File depFile = new File(outputFilePath+File.separator+depTableName+".xml");
IDataSet depData = new FlatXmlDataSetBuilder().build(depFile);
depData.getTable(depTableName);
System.out.println("IMPORT START TABLE NAME :"+depTableName);
try{
DeleteAllOperation.DELETE_ALL.execute(connection, depData);
DatabaseOperation.CLEAN_INSERT.execute(connection, depData);
}catch (Exception e){
e.printStackTrace();
}
importTableNames.add(depTableName);
}
}*/
FlatXmlDataSetBuilder builder = new FlatXmlDataSetBuilder();
builder.setColumnSensing(true);
IDataSet data = builder.build(inDirIsFile);
data.getTable(tableName);
try{
System.out.println("IMPORT START TABLE NAME :"+tableName);
/*DatabaseOperation op = new CompositeOperation(DatabaseOperation.DELETE_ALL,DatabaseOperation.INSERT);
op.execute(connection, data);*/
DatabaseOperation.CLEAN_INSERT.execute(connection, data);
importTableNames.add(tableName);
}catch (Exception e){
DatabaseOperation.DELETE_ALL.execute(connection, data);
}
}
}
}
}else{
String tableName = FilenameUtils.getBaseName(info.getName()) == null? "" : FilenameUtils.getBaseName(info.getName());
System.out.println("START FILE READ TABLE NAME :"+tableName);
if(importTableNames.contains(tableName)) continue; //이미 import된 xml이라면 다음으로.
outputFilePath = outputFilePath.replaceAll(info.getName(),"");
for(String getTableName : getTableNames){
if(getTableName.equals(tableName)){
FlatXmlDataSetBuilder builder = new FlatXmlDataSetBuilder();
builder.setColumnSensing(true);
IDataSet data = builder.build(info);
data.getTable(tableName);
data.getTable(tableName);
try{
System.out.println("IMPORT START TABLE NAME :"+tableName);
DatabaseOperation.CLEAN_INSERT.execute(connection, data);
importTableNames.add(tableName);
}catch (Exception e){
DatabaseOperation.DELETE_ALL.execute(connection, data);
}
}
}
}
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
public ArrayList<File> arrayDirInFileList(ArrayList<File> files ,File dir){
if(dir.isDirectory()) {
File[] children = dir.listFiles();
for(File f : children) {
// 재귀 호출 사용
// 하위 폴더 탐색 부분
arrayDirInFileList(files,f);
}
} else {
files.add(dir);
}
return files;
}
public void deleteDBData(){
IDatabaseConnection srm9qaDBConnection = null;
IDatabaseConnection srm9newDBConnection = null;
Statement stmt = null;
ResultSet rs = null;
try{
Connection jdbcConnection = DriverManager.getConnection("jdbc:oracle:thin:@url:1521:sid", "user", "pwd");
Connection srm9new_jdbcConnection = DriverManager.getConnection("jdbc:oracle:thin:@url:1521:sid", "user", "pwd");
srm9newDBConnection = new DatabaseConnection(srm9new_jdbcConnection,"user".toUpperCase());
srm9qaDBConnection = new DatabaseConnection(jdbcConnection,"user".toUpperCase());
DatabaseConfig srm9qaConfig = srm9qaDBConnection.getConfig();
srm9qaConfig.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new Oracle10DataTypeFactory());
srm9qaConfig.setFeature(DatabaseConfig.FEATURE_SKIP_ORACLE_RECYCLEBIN_TABLES, Boolean.TRUE);
//table 존재여부 검출용도
IDataSet databaseDataSet = srm9newDBConnection.createDataSet();
String[] getTableNames =databaseDataSet.getTableNames();
stmt = jdbcConnection.createStatement();
String sql = "db 자식-> 부모 순으로 지울수있는 쿼리";
rs = stmt.executeQuery(sql);
while(rs.next()){
QueryDataSet partialDataSet = new QueryDataSet(srm9newDBConnection);
String tableName = rs.getString("TABLE_NAME");
for(String getTableName : getTableNames) {
if (getTableName.equals(tableName)) {
System.out.println("DELETE TABLE NAME :"+tableName);
partialDataSet.addTable(tableName);
DatabaseOperation.DELETE_ALL.execute(srm9newDBConnection, partialDataSet);
}
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
if(null != srm9qaDBConnection) {
try{
srm9qaDBConnection.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
우선 import 시 초기화 -> insert 작업을 진행하려 하였는데,
현 회사에서는 oracle base로 data가 들어가야하는 순서 (부모-자식테이블) 구조가 있기 때문에,
초기화가 가능하도록 순차적으로 지울수있었으나, 활용하는 곳에서 지울수있는것은 미지수이기에,
이에 대한 방안은 고려해봐야할것으로 보인다.
또한 DBUnit을 디컴파일하여 처리하다보니, dataset xml 처리간 number / timestamp 에 대해서는 공백이 아닌 null 처리이면, column name 조차 표기하지 않아 data가 정확히 들어가지 않는 부분 있었다.
이는 datasetproduceradapter를 override하는것으로 해결하였다. ( 해당 부분은 xml로 데이터를 명확히 보려고하는 목적임을 서술합니다. )
public class DataSetProducerAdapter implements IDataSetProducer{
/**
* Logger for this class
*/
private static final Logger logger = LoggerFactory.getLogger(DataSetProducerAdapter.class);
private static final IDataSetConsumer EMPTY_CONSUMER = new DefaultConsumer();
private final ITableIterator _iterator;
private IDataSetConsumer _consumer = EMPTY_CONSUMER;
public DataSetProducerAdapter(ITableIterator iterator)
{
_iterator = iterator;
}
public DataSetProducerAdapter(IDataSet dataSet) throws DataSetException
{
_iterator = dataSet.iterator();
}
@Override
public void setConsumer(IDataSetConsumer consumer) throws DataSetException {
logger.debug("setConsumer(consumer) - start");
_consumer = consumer;
}
@Override
public void produce() throws DataSetException
{
logger.debug("produce() - start");
_consumer.startDataSet();
while(_iterator.next())
{
ITable table = _iterator.getTable();
ITableMetaData metaData = table.getTableMetaData();
_consumer.startTable(metaData);
try
{
Column[] columns = metaData.getColumns();
if (columns.length == 0)
{
_consumer.endTable();
continue;
}
for (int i = 0; ; i++)
{
Object[] values = new Object[columns.length];
for (int j = 0; j < columns.length; j++)
{
Column column = columns[j];
if(column.getDataType().isNumber() || column.getDataType().isDateTime()){
values[j] = table.getValue(i, column.getColumnName()) == null? null : table.getValue(i, column.getColumnName());
}else{
values[j] = table.getValue(i, column.getColumnName()) == null? "" : table.getValue(i, column.getColumnName());
}
}
_consumer.row(values);
}
}
catch (RowOutOfBoundsException e)
{
// This exception occurs when records are exhausted
// and we reach the end of the table. Ignore this error
// and close table.
// end of table
_consumer.endTable();
}
}
_consumer.endDataSet();
}
}
사용하는 방향에 따라 많은것을 진행할 수 있는 코드로 보여, 공유합니다.
잘쓰셨으면 좋겟습니다.