[Android] Basic concept of Room Database and Query example
- Android Room을 구성하는 세 가지 요소
- Data class (Entity)
- Foreign Key
- Data access objects (DAO)
- @Query
- @Insert
- @Update
- @Transaction
- RoomDatabase class
- CREATE TABLE
- DROP TABLE
- ALTER TABLE
- INSERT TABLE
Android Room을 구성하는 세 가지 요소
- data class
- dao
- room database
Database class에 dao를 선언한다. Database class에 dao를 통해서 데이터베이스 쿼리를 사용할 수 있다. 쿼리 데이터를 data class로 정의할 수 있다.
Data class (Entity)
데이터베이스에 테이블을 kotlin android에서 data class로 만들 수 있다. @Entity를 활용하여 테이블 이름, 기본키, 외래키에 대한 정의 등을 명시할 수 있다.
@Entity(
tableName = "some_table_name",
foreignKeys = [
ForeignKey(
entity = ParentObject::class,
parentColumns = ["parent_table_some_column_name"],
childColumns = ["some_table_column_name"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class SomeClassNameWhatYouWant(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long,
@ColumnInfo(name = "some_column_name") val someColumnName: String,
@ColumnInfo(name = "some_column_name") val someColumnName: Int,
@ColumnInfo(name = "some_column_name") val someColumnName: SomeObject = SomeObject.None,
@ColumnInfo(name = "some_column_name") val someColumnName: SomeObject = SomeObject,
@ColumnInfo(name = "some_column_name") val someColumnName: List<SomeObject> = listOf()
)
table을 구성하는 row 데이터를 식별하기 위해서 Primary Key가 반드시 필요하다. Primary Key는 해당 table에서 유일한 값이어야 한다. 그래야 데이터가 겹치지 않고 저장된다. Foreign Key는 필수 값이 아니다. 다른 테이블과 관계를 맺어야 한다면 Foreign Key를 정의할 수 있다.
Foreign Key
@Entity(
tableName = "some_table_name",
foreignKeys = [
ForeignKey(
entity = ParentObject::class,
parentColumns = ["parent_table_some_column_name"],
childColumns = ["some_table_column_name"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
some_table이라는 table이 있다. 이 table은 ParentObject data class가 구현한 어떤 테이블과 관계를 맺는다. 1:N 관계를 맺는데, parent_table_some_column_name라는ParentObject 테이블의 Primary Key 값을 some_table의 some_table_column_name와 매핑한다는 뜻이다. 그림으로 보면 이렇게 나타낼 수 있다.
관계를 맺고 있기 때문에 onDelete, onUpdate 가 발생했을 때 추가적인 행동 또한 명시할 수 있다. 예를 들어, parent_table의 특정 row 데이터가 사라지면 해당 데이터와 매핑되어 있는 다른 테이블의 데이터 또한 삭제되도록 만들 수 있다.
Data Access Objects (Dao)
@Dao 를 추가한 interface에 table에 대한 query문을 작성한다. 해당 query문으로 테이블에 값을 읽거나 쓸 수 있다.
@Dao
interface SomeObjectDao {
@Query("SELECT * FROM some_table")
fun getAllSomeObjects(): Single<List<SomeObject>>
@Transaction
@Query("SELECT * FROM some_table WHERE some_column = :value")
fun getSomeObjectWithSpecificValue(value: String): Single<List<SomeObject>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(list: List<SomeObject>): Completable
@Update
fun updateSomeObject(object: SomeObject): Completable
@Query("DELETE FROM some_table")
fun deleteAll(): Completable
}
@Query
Query문을 직접 입력해서 사용할 수 있는 어노테이션 기본적인 Query(get, insert, update, delete 등)는 어노테이션으로 제공되지만, 복잡한 쿼리문은 Query 어노테이션으로 사용할 수 있다.
@Insert
table에 새로운 데이터를 넣는다.
@Update
table에 있는 데이터를 갱신시킨다.
@Transaction
복합 쿼리문을 작성할 때 사용한다. 쿼리문만 보면 단순히 id값을 통해서 데이터를 불러오는 함수이지만, @Embedded, @Relation 어노테이션을 활용해 관계에 대한 정의한 data class를 리턴 값으로 지정하면 patient_id를 통해 Embedded 객체를 불러오는 쿼리, parent_patient_id와 관계를 맺고 있는 데이터 쿼리가 동시에 실행된다.
RoomDatabase
RoomDatabase를 상속받는 abstract class를 생성한다. 이 class는 데이터베이스 생성, 관리 등 데이터베이스의 전체를 담당하는 class이다. 데이터베이스에 들어가는 table(column이 명시된 data class)과 dao(query문이 작성된 인터페이스)를 명시하여 database의 인스턴스를 생성한다. 이렇게 생성된 인스턴스에 앞 서 명시한 파일들을 바탕으로 데이터를 읽거나 쓸 수 있게 된다.
@Database(
entities = [SomeObject::class, SomeObject::class],
version = 1,
exportSchema = true
)
@TypeConverters(MeasurementDataConverter::class) // List 형식 Column을 선언한 경우
abstract class AppDatabase : RoomDatabase() {
abstract fun someDao(): SomeDao
abstract fun someDao(): SomeDao
companion object {
private const val DATABASE_NAME = "your-database-name"
@Volatile private var instance: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return instance ?: synchronized(this) {
instance ?: buildDatabase(context).also { instance = it }
}
}
private fun buildDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) // 마이그레이션 코드를 삽입한 경우 추가
.build()
}
.....
데이터베이스가 변경되었다면 데이터베이스를 빌드할 때 변경된 사항을 알려줘야 한다. 이를 DB Migration 작업이라 한다. 데이터베이스 어노테이션에 version이 올라갈 때마다 Migration code가 하나씩 추가되는 것이 일반적이다. column, table의 이름을 바꾸거나 추가하거나 데이터를 옮기는 일 등이 여기에 해당한다. Migration 코드는 쿼리 문으로 작성한다.
...
companion object {
private const val DATABASE_NAME = "some-database"
@Volatile private var instance: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return instance ?: synchronized(this) {
instance ?: buildDatabase(context).also { instance = it }
}
}
private fun buildDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
}
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS some_table_tmp (" +
"'some_column' INTEGER PRIMARY KEY NOT NULL, " +
"'some_column' TEXT NOT NULL, " +
"'some_column' TEXT NOT NULL, " +
"'some_column' TEXT NOT NULL " +
"FOREIGN KEY('some_column') REFERENCES 'parent_table'('id') ON UPDATE CASCADE ON DELETE CASCADE" +
")")
database.execSQL("DROP TABLE some_table")
database.execSQL("ALTER TABLE some_table_tmp RENAME TO some_table")
}
}
private val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS parent_table_tmp (" +
"'id' TEXT PRIMARY KEY NOT NULL, " +
"'some_column' TEXT NOT NULL" +
")")
database.execSQL("INSERT INTO parent_table_tmp" +
"(" +
"id, some_column" +
") " +
"SELECT id, some_column " +
"FROM parent_table")
database.execSQL("DROP TABLE parent_table")
database.execSQL("ALTER TABLE parent_table_tmp RENAME TO parent_table")
}
}
}
CREATE TABLE
database.execSQL("CREATE TABLE table_name (" +
"'column_name' TEXT PRIMARY KEY NOT NULL, " +
"'column_name' INTEGER NOT NULL, " +
"'column_name' TEXT NOT NULL, " +
"'column_name' TEXT" +
")")
테이블을 생성하는 쿼리 문이다. 띄어 쓰기에 매우 유의해야 한다. 테이블명을 적고 column을 추가해서 쿼리 문으로 하나의 테이블을 생성할 수 있다. 보통 temp 형식의 임시 테이블을 만들고 기존 테이블에 데이터를 INSERT 한 후 이름을 변경하거나 하는 식으로 작업을 진행할 수 있다.
DROP TABLE
database.execSQL("DROP TABLE table_name")
삭제하고 싶은 테이블이 있을 때 DROP TABLE 로 특정 테이블을 삭제할 수 있다.
ALTER TABLE
database.execSQL("ALTER TABLE some_table_tmp RENAME TO some_table")
테이블에 어떤 변경 사항이 있을 때 ALTER TABLE 쿼리를 사용한다. 위의 예시는 table의 이름을 변경하는 쿼리 문이다.
INSERT TABLE
database.execSQL("INSERT INTO table_name" +
"(" +
"column_name, column_name, column_name, column_name, column_name" +
") " +
"SELECT " +
"column_name, column_name, column_name, column_name, column_name " +
"FROM table_name")
INSERT 다음 새로 만든 table name과 column name을 입력해서 어떤 테이블에 어떤 컬럼이 기존 데이터를 복사할지 정하고 SELECT 다음 기존 table name과 column name을 입력해서 어떤 값을 새로운 테이블에 복사해서 넣을 것인지 정하는 쿼리 문이다.