제가 개발하던 버스 정보 Application은 최초에 DB를 사용하지 않았습니다.
package net.hyeongkyu.sqlite;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class SqlExecutor {
/**
* @param args
*/
public static void main(String[] args) {
FileReader fr = null;
BufferedReader br = null;
Connection conn = null;
try{
// 두개의 부분을 바꾼다. dbUrl은 SQLite DB파일의 경로이고, fileDir은 실행시킬 SQL 문이 저장되어있는 txt 파일의 경로이다.
// SQL txt 파일에는 빈 행이 없어야한다.
//String dbUrl = "jdbc:sqlite:d:/database.db";
//String fileDir = "d:/sql.txt";
// 이 경로는 내 MAC OS X에서 사용하는 경로이다.
String dbUrl = "jdbc:sqlite:/Users/hyeongkyu/temp/database.db";
String fileDir = "/Users/hyeongkyu/temp/sql.txt";
Class.forName("org.sqlite.JDBC");
conn = DriverManager.getConnection(dbUrl);
fr = new FileReader(new File(fileDir));
br = new BufferedReader(fr);
int count = 0;
conn.setAutoCommit(false);
String sql = null;
Statement stmt = conn.createStatement();
while((sql=br.readLine())!=null){
stmt.executeUpdate(sql);
System.out.println(""+(++count));
}
conn.commit();
}catch(Exception e){
e.printStackTrace();
if(conn!=null) try{conn.rollback();}catch(Exception et){}
}finally{
if(br!=null) try{br.close();}catch(Exception e){}
if(fr!=null) try{fr.close();}catch(Exception e){}
}
}
}
소스 코드가 썩 보기 좋진 않지만, 위의 소스를 참고하면, 쉽게 SQLite DB에 대량의 명령어를 날릴 수 있습니다. 수만행을 수행하는데도 1~2초면 됩니다. 아주 기초적인 JDBC 예제의 수준을 벗어나지 않습니다.
해당 코드를 수행하기 위해서는 반드시 SQLite JDBC Driver가 필요하며, 이 또한 무료이며 간단한 검색으로 무진장 쉽게 구할 수 있습니다.
저렇게 완성이 된 SQLite 파일이 용량이 작다면 assets 경로에 저장하여 배포를 하게 됩니다.
파일의 크기가 1024kb가 넘는다면 1024kb 단위로 쪼개어서 assets에 저장한 후, Application이 최초에 실행되는 시점에서 합쳐야합니다.
그러나! DB 파일이 너무 크다면 이것을 정말 assets에 포함시킬 것인지에 대해 고려해보아야합니다.
일단 파일이 작다고 가정을 하겠습니다. 파일을 분할하고, 합치는 방법은, 아래와 같습니다.
package net.hyeongkyu.file;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class FileSeparator {
public static void main(String[] args){
// 아래의 두 상수는 분할할 파일의 경로와, 몇 바이트로 분할할 것인지를 정의합니다.
final String FILE_PATH = "d:\\database.db";
final long BYTE_OF_UNIT = 1048576;
File input = new File(FILE_PATH);
FileInputStream fis = null;
BufferedInputStream bis = null;
FileOutputStream fos = null;
BufferedOutputStream bos = null;
try{
fis = new FileInputStream(input);
bis = new BufferedInputStream(fis);
byte[] buf = new byte[4096];
int l = 0;
int acc = 0;
int count = 0;
while((l=bis.read(buf))>0){
if(acc%BYTE_OF_UNIT==0){
File output = new File(FILE_PATH+"."+(count++));
if(fos!=null){
bos.close();
fos.close();
}
fos = new FileOutputStream(output);
bos = new BufferedOutputStream(fos);
}
bos.write(buf, 0, l);
acc += l;
}
}catch(Exception e){
e.printStackTrace();
}finally{
if(bos!=null) try{bos.close();}catch(Exception e){}
if(fos!=null) try{fos.close();}catch(Exception e){}
if(bis!=null) try{bis.close();}catch(Exception e){}
if(fis!=null) try{fis.close();}catch(Exception e){}
}
}
}
/**
* 이 메서드는 Android Context 내에 정의되어 있어야합니다.
*/
private void combineFiles(){
// INPUT_ASSET_NAMES 병합할 assets 의 파일명을 나열합니다.
final String[] INPUT_ASSET_NAMES = new String[]{
"database.db.0", "database.db.1", "database.db.2", "database.db.3"
};
// OUTPUT_PATH에는 병합한 결과로 생성될 파일의 경로를 할당합니다.
final String OUTPUT_PATH = Environment.getExternalStorageDirectory().getAbsolutePath()+"/data/net.hyeongkyu.android.incheonBus/database.db";
final File OUTPUT = new File(OUTPUT_PATH);
InputStream is = null;
BufferedInputStream bis = null;
FileOutputStream fos = null;
BufferedOutputStream bos = null;
try{
fos = new FileOutputStream(OUTPUT);
bos = new BufferedOutputStream(fos);
for(int i=0;i<INPUT_ASSET_NAMES.length;i++){
is = getApplicationContext().getAssets().open(INPUT_ASSET_NAMES[i]);
bis = new BufferedInputStream(is);
int l=0;
byte[] buf = new byte[4096];
while((l=bis.read(buf))>0) bos.write(buf, 0, l);
try{bis.close();}catch(Exception e){}
try{is.close();}catch(Exception e){}
}
}catch(Exception e){
e.printStackTrace();
}finally{
if(bos!=null) try{bos.close();}catch(Exception e){}
if(fos!=null) try{fos.close();}catch(Exception e){}
if(bis!=null) try{bis.close();}catch(Exception e){}
if(is!=null) try{is.close();}catch(Exception e){}
}
}
역시 기초적인 java.io 예제 수준을 벗어나지 않습니다.
저러한 식으로 파일을 분할하여 assets에 포함시켰다가, Application이 실행될 때 다시 합쳐서 외부 저장공간(일반적으로 SD Card)으로 옮겨버리면 되는 것입니다.
그러나!
파일의 크기가 커지기 시작하면 DB 파일을 정말로 assets에 포함시킬 것인가에 대해 고민해볼 필요가 있습니다. assets에 포함된 파일은 apk에 포함되어 배포가 됩니다.
결과적으로 apk 파일이 커지게 되고, assets에 포함된 파일은 Read only이므로 더 이상 사용하지 않는다하더라도 삭제가 되지 않습니다.
3MB 정도의 DB가 3개의 파일로 분리되어 assets에 포함되었다고 치면, 결국 이 파일은 병합되어 External Storage로 옮겨질 것입니다. 그리고 Application에서는 병합된 파일만을 참조할 것이지만, assets에 포함되어 있던... 이제는 더 이상 쓸모없는 파일은 삭제가 되지 않고 여전히 내부 저장공간을 차지하고 있다는 얘기죠.
이에 대한 훌륭한 대안으로, Google Code의 Project Hosting이라는 서비스가 있습니다.
http://code.google.com/hosting
이 서비스 내에는 Download hosting이라는 서비스도 포함되어 있어서, 배포하고 싶은 파일을 미리 올려놓을 수 있습니다.
저 또한 이 서비스를 이용하여 버스 정보 DB를 배포하고 있습니다.
http://code.google.com/p/android-korea-bus-information
하지만, 여기에 올린 파일은 어떠한 보안도 적용되지 않고 누구나 열람할 수 있게 됩니다.
업로드, 삭제 등의 기능은 계정의 관리자만이 가지게 되지만, 다운로드의 기능은 누구나 가지게 된다는 것입니다. 이 점은 알아두셔야합니다.
이 호스팅에서 제공하는 Download 기능을 이용하면, 파일을 업로드 해놓고 URL을 얻어낼 수 있습니다.
게다가 알아보기 쉬운 주소로 나오게 되며, 파일명이 변하지 않을 경우에는 파일을 교체하더라도 URL이 변하지 않기 때문에 매우 효율적이죠.
// S.DB_DOWNLOAD_URL DB 파일을 다운로드하게 될 주소 예를 들어, "http://android-korea-bus-information.googlecode.com/files/db.zip"
// S.DB_DATE 현재 버젼의 DB 날짜, 예를 들어 20100728
// Utility.unzip() 압축 해제를 위해 따로 정의한 유틸리티 메서드
final Thread thread = new Thread(new Runnable(){
@Override
public void run() {
try{
URL url = new URL(S.DB_DOWNLOAD_URL);
URLConnection connection = url.openConnection();
int length = connection.getContentLength();
progress.setMax(length);
InputStream is = null;
FileOutputStream fos = null;
BufferedOutputStream bos = null;
boolean success = false;
try{
is = connection.getInputStream();
File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/data/net.hyeongkyu.android.incheonBus");
if(!dir.exists()) dir.mkdirs();
File target = new File(dir.getAbsolutePath()+"/database."+S.DB_DATE+".zip");
if(!target.exists()) target.createNewFile();
else{
target.delete();
target.createNewFile();
}
fos = new FileOutputStream(target);
bos = new BufferedOutputStream(fos);
byte[] buf = new byte[1024];
downloadFlag = true;
int l = 0;
while((l=is.read(buf))>0){
tmpLength += l;
Log.i(S.TAG, tmpLength+" received.");
if(!downloadFlag) break;
bos.write(buf, 0, l);
activity.runOnUiThread(new Runnable() {
public void run() {
progress.setProgress(tmpLength);
}
});
}
success = true;
}catch(Exception e){
e.printStackTrace();
}finally{
try{bos.close();}catch(Exception e){}
try{fos.close();}catch(Exception e){}
try{is.close();}catch(Exception e){}
dialog.dismiss();
wakeLock.release();
}
// 성공 시 압축 해제하고, 파일명 변경
if(success){
File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/data/net.hyeongkyu.android.incheonBus");
File target = new File(dir.getAbsolutePath()+"/database."+S.DB_DATE+".zip");
Utility.unzip(target, dir, false);
// 파일명을 변경한다.
File[] listFiles = dir.listFiles();
for(File file:listFiles){
if(file.getName().toLowerCase().endsWith(".db")){
file.renameTo(new File(dir.getAbsolutePath()+"/database."+S.DB_DATE+".db"));
}else{
file.delete();
}
}
activity.runOnUiThread(new Runnable(){
@Override
public void run() {
Toast.makeText(activity, S.MSG_DB_DOWNLOAD_COMPLETE, Toast.LENGTH_SHORT).show();
}
});
}
}catch(Exception e){
e.printStackTrace();
}
}
});
thread.start();
위의 코드는 미리 올려놓은 DB 파일을 내려받아 External Storage에 특정 디렉토리로 압축을 해제시키는 코드이며, 전국 버스 앱에서 사용하고 있는 코드입니다.
이렇게 내려받은 SQLite DB 파일은 Android에서 다음과 같이 Open 시킬 수 있습니다.
public class IncheonBusDbAdapter {
private Context mContext;
private SQLiteDatabase mDb;
public IncheonBusDbAdapter(Context context, int accessFlags) {
super();
this.mContext = context;
File dbCopy = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/data/net.hyeongkyu.android.incheonBus/database."+S.DB_DATE+".db");
if(dbCopy.exists()){
SQLiteDatabase db = SQLiteDatabase.openDatabase(dbCopy.getAbsolutePath(), null, accessFlags);
this.mDb = db;
}
}
public void close(){
this.mDb.close();
}
// ... 후략 ...
그럼 모두 즐 Android Programming 하십시오!
감사합니다!
'IT_Programming > Android_Java' 카테고리의 다른 글
Intent Action & Category (0) | 2014.05.27 |
---|---|
[Android] Way to avoid using 'notifyDataSetChanged' when only one or two items are changed at Adapter. (0) | 2014.05.16 |
이어받기 기능이 적용된 File Downloader (0) | 2014.05.10 |
A Comparison of java.net.URLConnection and HTTPClient (0) | 2014.05.10 |
[Android] WebView에서 <a> 태그의 "_blank" target 처리하기. (0) | 2014.05.07 |