-
[Android Studio]How to use pre-populated .db file in Room(미리 만든 db파일, 앱에서 local로 쓰기)Android/Usage 2019. 7. 21. 22:51
처음에 삽질 했던 것이 기억나서 글로 남겨봅니다. 틀린 정보 있을겁니다.. 뇌피셜로 이해 한 것들이라..
(sdk 26~28, kotlin, android studio에서 작성)
Room은 Google에서 Local Database인 SQLite 쓸 때 더 편하라고 만든 ORM입니다. 이게 다른 Jetpack에 들어있는 LiveData, Viewmodel, Lifecycle 등을 지원하기 때문에 요러요러한 것들 쓰는 사람이라면 편합니다!
ORM이란
Object-Relational Mapping의 약자입니다. 아마 백엔드에서 DB 만져보신 분들은 아시는 개념일겁니다. Object는 객체지향언어 할 때 그 객체고, Relational은 RDB의 관계형 DB 할 때 그 관계입니다. 옛날엔 코딩하면서 IDE에서 MySQL같은 DB 쓸라면 복잡했나 봅니다. 그래서 그 사이를 떨어트리게 해주는? 그런 개념입니다. DB를 객체처럼 쓰는 느낌?
여튼 제가 학교 맛집 어플 만들면서 이용자가 많을 것이라 예상(...)하여 프리티어 서버로는 감당이 안 될 것이라 판단, Local DB를 쓰기로 마음먹어서 쓰게 됐습니다.
(아 물론 지금은 서버가 100배 낫다는걸 깨달았기에 바꾸려고 합니다...흑)제가 사용한 방식은 DB Browser로 DB파일을 따로 만들고, 그걸 가져와서 사용하는 방식입니다. 아예 코드 내에서 테이블 만들고 데이터 넣고 등등은 다른 블로그 찾아보면 잘 나와있더라구요!
일단 Module의 build.gradle에서 요것들을 넣어줍니다.
apply plugin: 'kotlin-kapt' implementation 'androidx.room:room-runtime:2.1.0-alpha06' kapt 'androidx.room:room-compiler:2.1.0-alpha06'
(아마 라이브러리 저거 넣으면 최신버전으로 바꾸라고 뭐 메세지 뜰겁니다. 그걸로 바꿔주시면 됩니다~)
그리고 구글 검색에 DB Browser를 다운 받으시고, 키셔서 테이블 만들고 데이터도 넣고 만들고 싶은데로 만듭시다.
(사용법은 검색~) 그리고 만든 .db파일을 assets란 폴더를 프로젝트에서 만들어 databases폴더를 또 만들어 넣어줍니다.
요런식으로 프로젝트 우클릭 해서 폴더 만드세요. DB 패키지 구성입니다. 그리고 제 프로젝트에 DB 관련한 것들은 패키지로 묶어서 보관하고, 위와 같이 4개의 파일로 구성되어 있습니다.
AppDatabase
Room으로 데이터베이스 정의하는 파일입니다.
DatabaseCopier
DB Browser로 만든 .db 파일을 제 모바일 기기에 직접 심기 위한 파일입니다.
DBEntity
DB의 Table들을 정의 해 놓는 파일입니다. 이거 좀 귀찮;
RawDAO
쿼리문들을 정의 해 놓는 파일입니다. DAO가 Data Access Object의 약자인데, DB의 data에 access하는 Transaction이랍니다. 위에 설명 한 것 처럼 중간에서 쿼리문 대신 날려주고 처리해주는 느낌?
DBEntity.kt
import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "chicken") data class ChickenEntity(@PrimaryKey val name: String, val menu: String, val latitude: String, val longitude: String, val phone: String) @Entity(tableName = "chinese") data class ChineseEntity(@PrimaryKey val name: String, val menu: String, val latitude: String, val longitude: String, val phone: String) @Entity(tableName = "curry") data class CurryEntity(@PrimaryKey val name: String, val menu: String, val latitude: String, val longitude: String, val phone: String) @Entity(tableName = "cutlet") data class CutletEntity(@PrimaryKey val name: String, val menu: String, val latitude: String, val longitude: String, val phone: String) ...
데이터베이스에 넣어논 Table들을 정의합니다. ...으로 표시한 건 한 12개 다 같은 테이블들이라... 여튼 table 이름을 annotation에 쓰고, 각 테이블에 어떤 컬럼들이 있는지 쓰면 됩니다.
AppDatabase.kt
import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase @Database(entities = [KoreanEntity::class, ChineseEntity::class, CutletEntity::class, ChickenEntity::class, PizzaEntity::class, InstantEntity::class, HamburgerEntity::class, LunchboxEntity::class, SoupEntity::class, NoodleEntity::class, SushiEntity::class, MeatEntity::class, RestaurantEntity::class, CurryEntity::class], version = 2, exportSchema = false) abstract class AppDatabase : RoomDatabase(){ abstract fun rawDAO(): RawDAO companion object { @JvmField val MIGRATION_1_2 : Migration = object : Migration(1, 2){ override fun migrate(database: SupportSQLiteDatabase) { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } } } }
데이터베이스 자체를 여기서 정의합니다. annotation에다가 DBEntity에 쓴 테이블들을 다 꾸겨넣습니다. 끝에 version은 원래라면 1로 쓰시면 되는데, 저 처럼 외부에서 가져올 경우 2로 써주세요. 그리고 RoomDatabase()를 상속하고, 나중에 쓸 DAO를 abstract fun 타입으로 선언 해놓습니다.
그리고 여기서 중요한게 companion object 블럭 안에 코드들입니다. companion object는 자바의 static method랑 비슷한 개념입니다. 한 번 선언하면 해당 클래스를 다시 호출하거나 생성해도 재생성 안하는, static한 느낌이죠.
여튼 이 안에 migration 단어가 보이죠? 뜻 그대로 이주시킨다는 건데, 이건 DB의 버전 자체를 관리하는 메서드입니다. 1_2가 1버전에서 2버전으로 간다는 뜻이죠. 보통 DB 파일을 외부에서 안넣고, 자체적으로 만드는 경우라면 이 migration이 필요가 없습니다. (애초에 버전이 1이라)
근데 저의 경우 외부에서 가져왔기에, 코드 자체에서 만든 DB가 버전 1이고 외부에서 가져온 DB 형식을 옮기기 때문에(버전2) 이러한 이주?가 필요한 겁니다.
원래 저 안에 TODO라 쓰인 곳에는 버전 업글 할 때 컬럼이 추가됐다느니, Primary Key가 뭐가 바뀌었다느니 등등 선언하는 곳 입니다. 근데 저는 그냥 옮기기만 하는거라 노상관!
RawDAO.kt
import androidx.room.Dao import androidx.room.RawQuery import androidx.sqlite.db.SupportSQLiteQuery import com.example.skku_food.data.ResSimpleData import com.example.skku_food.data.ResFullData @Dao interface RawDAO{ @RawQuery fun getJustNamePhone(query: SupportSQLiteQuery): List<ResSimpleData> @RawQuery fun getFullResInfo(query: SupportSQLiteQuery): ResFullData }
쿼리문 만드는 곳 입니다. 위와 같은 양식으로 인터페이스를 만들어주면 되는데, 제가 쓴 방법은 매우 꿀팁 방식입니다!
DAO 작성하는 글들을 구글에서 찾아보면, 대부분 쿼리문 하나하나 작성하는 방식으로 되어 있습니다.
(예시 - 이건 Java 언어로 돼있네요)
@Query("SELECT * FROM user") List<User> getAll();
여튼 저는 Raw Query라고 해서, 문자열로 쿼리문을 저기다 날려주면 아~무 쿼리문이나 작동 되게 할 수 있는겁니다! Select하랴 Updata하랴 하나하나 만들기 정말 귀찮죠..
(특히 저는 같은 쿼리문을 테이블 이름만 바꿔서 12개 정도 만들어야 했기에...)
구글 개발자 문서에서 찾은 내용이니 유용하게 쓰세요!!
(Raw Query를 날리는 방법은 맨 아래에 나와있습니다)(ResSimpleData나 ResFullData는 제가 만든 데이터 클래스입니다. 그냥 리턴값 원하는거 만들라는 소리에요~ㅎㅎ)
DatabaseCopier.kt
import android.content.Context import android.content.pm.PackageInfo import android.util.Log import androidx.core.content.pm.PackageInfoCompat import androidx.room.Room import java.io.File import java.io.FileOutputStream import java.io.IOException object DatabaseCopier { private val TAG = DatabaseCopier::class.java.simpleName private const val DATABASE_NAME = "food.db" private var INSTANCE: AppDatabase? = null fun getAppDataBase(context: Context): AppDatabase?{ if (INSTANCE == null){ Log.d(TAG, "인스턴스 null") synchronized(AppDatabase::class){ INSTANCE = Room.databaseBuilder( context, AppDatabase::class.java, DATABASE_NAME ) .addMigrations(AppDatabase.MIGRATION_1_2) .build() } } else{ Log.d(TAG, "인스턴스 null 아님") } return INSTANCE } // assets -> 앱/databases fun copyAttachedDatabase(context: Context) { Log.d(TAG, "db 카피 함수 호출") val dbPath = context.getDatabasePath(DATABASE_NAME) Log.d(TAG, dbPath.toString()) // db 파일 있으면 if (dbPath.exists()) { Log.d(TAG, "db 카피 이미 존재") val info: PackageInfo = context.packageManager.getPackageInfo(context.packageName, 0) val version = PackageInfoCompat.getLongVersionCode(info) Log.d(TAG, version.toString()) // 버전 관리 (계속 변경) if (version.toString() != "1"){ Log.d(TAG, "버전 코드 다름!") copyDB(context, dbPath) } return } Log.d(TAG, "db 카피 존재 x") // 폴더 만들어주기 dbPath.parentFile.mkdirs() copyDB(context, dbPath) } // Copy Function private fun copyDB(context: Context, _dbPath: File){ try { val inputStream = context.assets.open("databases/$DATABASE_NAME") val output = FileOutputStream(_dbPath) val buffer = ByteArray(8192) var length: Int while (true){ length = inputStream.read(buffer, 0, 8192) if(length <= 0) break output.write(buffer, 0, length) } output.flush() output.close() inputStream.close() } catch (e: IOException) { Log.d(TAG, "copyDB 실패!", e) e.printStackTrace() } } }
(여기가 좀 빡셉니다. 삽질도 많이 했습니다..)
object로 만든 건 위의 companion object처럼 한 번만 실행하려고 한 거구요,
getAppDatabase()
RoomDatabase 인스턴스를 얻기 위한 메서드입니다. Return 값으로 AppDatabase를 반환하는데, 위에 작성한 AppDatabase.kt 자체가 데이터베이스가 되기에 그런 겁니다.
여튼 INSTANCE 변수에 항상 담다가, 맨 처음 null인 상태에선 만들어주고, 아니면 그냥 그대로 리턴하는 식 입니다. (object 선언이라 static 성질)
Room.databaseBuilder 빌더로 쭉 만들어 주시면 됩니다. (migration 넣는거 잊지 마세요!)
synchronized는 스레드 동기화인데(A스레드가 이용 중일 때 B스레드가 lock때문에 못쓰게 하는거) 저도 찾았던 해외 글에서 쓰던데 왜 쓰는지 잘 이해 안갑니다ㅋㅋㅋ DB 만들 때 스레드로 만드나?;
copyAttachedDatabase()
dbPath 변수에 각 앱마다 DB파일을 놓는 폴더 경로를 뽑아냅니다. 그래서 이미 있을 경우와 아닐 경우를 나누어 놓았는데, 막 version 써있죠? 그건 제가 local DB가 업데이트 방식이 플레이스토어에 업데이트 하고 사용자가 직접 업데이트를 해야 하는 번거로움 때문에 만들어논 겁니다.. 안쓰셔도 돼요.. 나중에 서버로 바꾸고 싶당
여튼 존재 안하면 copyDB()를 호출해서 그 경로에 복사를 합니다.
copyDB()
inputstream으로 assets 안에 있는 .db파일, output으로 넣을 경로를 설정 해 줍니다. 그리고 8KB씩 for문 돌려서 옮겨주면 됩니다~
(옛날 글에는 1KB씩 밖에 못 옮긴다고 써져있더라구요... 근데 8KB해보니까 됨)
자 이제 준비는 끝났습니다~ 그럼 어떻게 쓰냐구요? 이제 MainActivity.kt 같은 시작 화면에서 DB 옮기는 저 Copier 호출하면 됩니다.
// onCreate()에서 아래 코드 써주시고 job = CoroutineScope(Dispatchers.IO).launch{ DatabaseCopier.copyAttachedDatabase(context = applicationContext) } runBlocking { job.join() }
override fun onDestroy() { job.cancel() super.onDestroy() }
위와 같이 쓰레드(저는 코루틴 사용) 써서 쓰시면 됩니다. Job이라는 자료형을 썼는데, 만약 쓰레드로 db파일을 복사하다가(오래걸린다면) 강제종료 시 같이 종료 하라고 해놓은 사소한 안전장치입니다.
여튼 그 후에 사용법은
val query = SimpleSQLiteQuery("SELECT * FROM $menu_name where name = '$res_name'") val db = DatabaseCopier.getAppDataBase(context = applicationContext)
위 처럼 날릴 쿼리문 Raw하게 만들어 주시고, db변수에 인스턴스를 넣어 줍시다! 그리고 나서
resInfo = db!!.rawDAO().getFullResInfo(query)
위 처럼 db 인스턴스에 쿼리 메서드들 호출하면서 쓰면 됩니다!
대충 검색해보시면 관련 해외 글들이 많이 있는데, 제 글에서만 찾을 수 있는 특이 정보라면 RawQuery 쓰는거랑 DB파일 복사할 때 migration 하는 정도가 있겠네요. 그래도 한글로 정리해 놓았기 때문에 도움이 될겁니다...!
'Android > Usage' 카테고리의 다른 글
Android LocalDate/Time(안드로이드 날짜/시간 관련 메서드) (0) 2019.10.24 Android Fragment Usage(안드로이드 프래그먼트 사용법) (0) 2019.10.21