개요
Assets 에서 데이터베이스 를 비교 하고 복사하는 코드.
kotlin 으로 되어 있습니다. 필요해져서 만들게 된 코드 입니다.
SQLiteAssetHelper 라는 라이브러리도 있는 듯 한데, 저 같은 경우는, 사실 이 아래에 적힌 것보다 더 많이 복잡한 루틴이 들어갔기 때문에, 직접 만들게 되었습니다. (ㅠㅜ)
참고하실 분을 위해서 적어둡니다.
본문
(주의) 아래에 있는 코드는 필요한 부분만 남겨서 잘라낸 코드 입니다. 실제 동작시 빠져있는 부분이 있을 수 있습니다.
import android.content.Context
import android.content.SharedPreferences
import android.database.sqlite.SQLiteDatabase
import android.preference.PreferenceManager
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
/**
*/
class AppDatabaseAssetsHandler (context: Context){
// context 레퍼런스
private val mContext = context
// 앱에서 실행되는 Database File 의 이름
private val mDatabaseName = "app_database.db"
// 앱에서 실행되는 Database File 의 전체 경로
private var mDatabasePath :String
// 앱에서 실행되는 Database 의 버전의 설정값 이름
private val versionPreferencesKey = "DB_CURRENT_VERSION"
// assets 의 DB 파일의 경로
private val assetDbFilePath = "db"
// assets 의 DB 파일의 파일명
private val assetDbFileName = "app_database.db"
// assets 의 DB 파일의 버전을 담고 있는 파일
private val assetDbVersionFileName = "app_database.db.version"
init{
mDatabasePath = context.getDatabasePath(mDatabaseName).path
initialize()
}
/**
* 데이터베이스 파일을 생성하고 업데이트 하는 메서드
* 디비 파일이 없으면, 디비 파일을 복사해 온다.
* 디비 버전을 체크해서, Assets 에 있는 DB 가 최신 것이라면 복사해온다.
* @todo 최초 설치와 업데이트 설치에만 Assets 에서 복사해 오도록 변경할 필요 있음. (급하지는 않음. 지금도 속도는 충분)
*/
private fun initialize(){
// 설정 관리자
val prefs = PreferenceManager.getDefaultSharedPreferences(mContext)
// Local 데이터베이스가 있는지 확인하고, 분기를 나눔.
if(!existsDatabaseLoaded(mContext)){
// 보통 첫 설치 때의 동작. 속도가 느릴 수 있으므로, 최대한 간단히 동작하도록 한다.
debug("[open] 데이터베이스가 없음")
// assets 디비 파일이 있는지 체크하고, 있을 시에 복사함
try {
if (existsAssetsDatabase(mContext)) {
// 오래된 버전의 기기인 경우 databases 디렉토리를 생성 안 하는 버그가 있다. 이 경우를 대비해서 디렉토리를 미리 생성
File(mDatabasePath).also{
file -> file.parentFile.mkdirs()
}.createNewFile()
debug("[open] Assets 에서 데이터베이스 파일 복사 실행")
copyDatabaseFromAssets(mContext)
debug("[open] getAssetDbVersion")
val assetDbVersion = getAssetDbVersion(mContext)
debug("[open] setAppDbVersionPreference")
setAppDbVersionPreference(prefs, assetDbVersion)
}
} catch (e: Exception){
debug("[open] Assets 에서 복사 실패 ",e)
}
} else {
// 앱을 실행하거나, 업데이트 했을 때 등의 동작.
debug("[open] 데이터베이스가 존재함")
val localDbVersion = getAppDbVersionPreference(prefs)
val assetDbVersion = getAssetDbVersion(mContext)
debug("[open] 실행 초기 로컬 DB 버전 : ",localDbVersion)
debug("[open] Assets 에 위치한 DB 버전 : ",assetDbVersion)
// assets 의 버전이 높을 경우, assets 의 파일을 다운로드 함.
if(localDbVersion < assetDbVersion){
try {
// assets 디비 파일이 있는지 체크하고, 있을 시에 복사함
if(existsAssetsDatabase(mContext)) {
debug("[open] Assets 에서 데이터베이스 파일 복사 실행")
copyDatabaseFromAssets(mContext)
setAppDbVersionPreference(prefs,assetDbVersion)
}
} catch (e: Exception) {
debug("[open] Assets 에서 복사 실패 ",e)
}
}
}
}
/**
* 데이터베이스 경로를 리턴.
*/
fun getDatabasePath() : String{
return mDatabasePath
}
/**
* Preferences 에서 App DB 버전 값을 가져옴.
* PreferenceManager 를 여러번 이용할 경우에는
* getAppDbVersionPreference(prefs : SharedPreferences) 를 이용하기를 권장함.
*/
@Suppress("unused")
private fun getAppDbVersionPreference() : Int{
val prefs = PreferenceManager.getDefaultSharedPreferences(mContext)
return getAppDbVersionPreference(prefs)
}
/**
* Preferences 에서 App DB 버전 값을 가져옴
*/
@Suppress("unused")
private fun getAppDbVersionPreference(prefs : SharedPreferences) : Int{
return prefs.getInt(versionPreferencesKey,0)
}
/**
* 버전 정보값을 Preferences 에 저장 하는 메서드.
*/
private fun setAppDbVersionPreference(prefs : SharedPreferences, version:Int){
val editor = prefs.edit()
editor.putInt(versionPreferencesKey,version)
editor.apply()
}
/**
* Assets 에 있는 Version 이 담긴 text 파일에서 버전 정보를 읽어온다.
* 에러가 있을 경우 그저 0 을 반환한다.
*/
private fun getAssetDbVersion(context:Context):Int{
return try {
Integer.parseInt(readTextFromAssets(context,"$assetDbFilePath/$assetDbVersionFileName"))
} catch (e: Exception) {
0
}
}
/**
* File 에서 version 정보를 가져오는 메서드
* 연결을 해야하기 때문에 속도가 느릴 수도 있음...
*/
@Suppress("unused")
private fun getVersionFromDatabaseFile(file: File): Int{
// 파일의 존재 유무를 먼저 체크한다. 그런데 이미 file 개체로 넘어왔으니 있을 것 같은데..
return if(file.exists())
{
val db:SQLiteDatabase = SQLiteDatabase.openOrCreateDatabase(file,null)
val version = db.version
db.close()
version
} else {
0
}
}
/**
* 현재 로드된 데이터베이스 가 존재하는지 여부 확인.
* 특별한 경우가 없다면, 최초 1회 확인만 하게 될 듯.
* 인수로 Context 를 넘겼는데, 혹시 모르니 불안해서 넣은 것임. 그대로 둘 것.
* (데이터베이스명은 문자열로 들어갔고 변경여지가 없으나, Context 는 때에 따라서는 바뀌는 변수가 될 수 있음)
* 데이터베이스를 open 해서 try 로 확인하는 방법이 좀 더 유용하지만,
* 단순히 file 의 존재만 확인 하는 것으로 하기로 함.
*/
private fun existsDatabaseLoaded(context:Context): Boolean{
val dbFile = context.getDatabasePath(mDatabaseName)
//debug("[existsDatabaseLoaded]",dbFile.path)
return dbFile.exists()
}
/**
* Assets 에 데이터베이스 파일이 존재하는지 유무.
*/
private fun existsAssetsDatabase(context:Context):Boolean {
return existsAssetFile(context,assetDbFilePath,assetDbFileName)
}
/**
* Assets 에 있는 DB 파일을 local DB 에 복사.
*/
@Throws(IOException::class)
private fun copyDatabaseFromAssets(context:Context){
copyFromAssets(context, "$assetDbFilePath/$assetDbFileName", mDatabasePath)
}
/**
* Assets 에 파일이 있는지 확인하는 메서드.
* assets 에서 한 단계 밑의 폴더 내의 파일을 확인할 수 있다.
* 두 단계 이상으로는 구현 안 함. 오히려 비효율적일 수 있어서.
*/
private fun existsAssetFile(context:Context,path:String,fileName:String):Boolean {
return context.assets.list(path).toList().contains(fileName)
}
/**
*
*/
private fun readTextFromAssets(context:Context, assetURI:String) : String{
return context.assets.open(assetURI).bufferedReader().use {
it.readText()
}
}
/**
* Assets 에서 파일을 복사하는 메서드.
* 일반적으로 사용 가능함.
*/
@Throws(IOException::class)
private fun copyFromAssets(context:Context, assetURI : String, toPath:String){
FileOutputStream(toPath).use { out ->
context.assets.open(assetURI).use {
it.copyTo(out)
}
}
}
/**
* 디버깅 메서드
* @param msg 메시지
*/
@Suppress("unused")
private fun debug(msg: String, msg2 : Any = "") {
@Suppress("ConstantConditionIf")
if (isDebug) {
Log.d(TAG, "$msg $msg2")
}
}
companion object {
private const val TAG = "[AppDatabaseAssetsHandler]"
private const val isDebug = false
}
}
변수 값 설명
mDatabaseName : 앱 내부에 있게될 db 파일명
versionPreferencesKey : 현재의 디비 버전 값을 넣어둘 Preference 키
assetDbFilePath : assets 에 넣어둘 때, 디렉토리 경로. (폴더 한 단계로만 해주세요... 저는 db 폴더를 만들고 넣었습니다)
assetDbFileName : assets 에 넣어둘 db 파일명
assetDbVersionFileName : assets 에 넣어둘 db 파일의 버전 값 (단순히 숫자만 넣어주면 됨)
이 값들을 수정하면 됩니다.
향후에는 assets 에 db 파일을 수정해서 넣고, version 도 텍스트 문서에 적어서 변경해주고 넣어주면, 이것으로 비교를 합니다.
(assets 의 db 파일을 직접 열어서 그 안의 version 값을 비교하고 싶은데, 오히려 비효율적일 것 같아서 그렇게 안 했습니다.. 너무 번거로워져요...)
assets
- db
- blah.db : 디비 파일.
- blah.version : 버전 값을 숫자로 기입.
호출
중요한 것은 호출입니다. 설명이 어려울 수 있는데....
A 라는 싱글턴 클래스를 만들고. (저는 Object 로 만들었습니다.. 귀찮아서.. )
MainActivity 의 onCreate 에서 A 클래스의 load 를 호출합니다.
A 클래스의 load 라는 메서드를 만들고 그 안에서 아래와 같은 구문을 넣었습니다.
appDatabasePath 는 A 클래스의 멤버 변수 입니다.
// appDatabasePath 는 만든 멤버변수
if(appDatabasePath==""){
doAsync {
//디비 연결 및 생성과 Assets 을 통한 업데이트
appDatabaseHandler = AppDatabaseAssetsHandler (context.applicationContext)
// 데이터베이스의 경로만 갖는다.
appDatabasePath = appDatabaseHandler.getDatabasePath()
}
}
appDatabasePath 라는 멤버 변수를 하나 만들어서, 여기에 값이 아직 들어오기 전 일 때에만 동작하도록 처리를 하였습니다.
이렇게 하면, 앱 실행 처음 한번에만 동작되게 됩니다.
참고
doAsync 는 'anko' 의 '라이브러리'를 사용한 것입니다... 이것까지 설명하려면.. doAsync 를 사용 안 하셔도 됩니다. 이것은 비동기호출을 하기 위한 부분이라서, 있으면 좋고 없어도 되는 부분입니다. (속도 차이는 분명합니다..)
doAsync 에 대해서는 아래를 참고해주세요. (구글링)
https://www.google.com/search?q=android+doasync
doAsync 를 사용안하시고, 일반적으로 사용하는 AsyncTask 를 이용하셔도 됩니다.
'개발 > Android' 카테고리의 다른 글
[Android] java.lang.NoClassDefFoundError: aewt (3) | 2020.12.14 |
---|---|
[Android Studio] Local scopes 와 Shared scopes 차이 (0) | 2020.10.09 |
[안드로이드 Android] 내부 테스트 트랙 걸리는 시간 (0) | 2019.03.29 |
[안드로이드 스튜디오 Android studio] 에디터 폰트 사이즈 조정. (0) | 2019.03.28 |
[안드로이드 Android] SQLite 에 대해서 (0) | 2019.03.11 |
[안드로이드 스튜디오 Android Studio] 코드 검사 시 불필요한 것 (google-services.json 등) 제외하기 (0) | 2019.02.22 |
[안드로이드 Android] applicationId, application id 변경 (1) | 2019.02.21 |
[Android] Error loading project. cannot load module '~~' (0) | 2019.02.21 |