앱에서 상당한 양의 구조화된 데이터를 처리하는 경우 데이터를 로컬로 유지하여 대단한 이점을 얻을 수 있습니다. 가장 일반적인 사용 사례는 기기가 네트워크에 액세스할 수 없을 때도 사용자가 오프라인 상태로 계속 콘텐츠를 탐색할 수 있도록 관련 데이터를 캐시하는 것입니다.
Room 지속성 라이브러리는 SQLite를 완벽히 활용하면서 원활한 데이터베이스 액세스가 가능하도록 SQLite의 추상화 계층을 제공합니다.
Room 2.x 설정
앱에서 Room 2.x를 사용하려면 앱의 build.gradle 파일에 다음 종속 항목을 추가합니다.
dependencies {
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
annotationProcessor("androidx.room:room-compiler:$room_version")
// To use Kotlin Symbol Processing (KSP)
// ksp("androidx.room:room-compiler:$room_version")
// optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$room_version")
// optional - RxJava2 support for Room
implementation("androidx.room:room-rxjava2:$room_version")
// optional - Guava support for Room, including Optional and ListenableFuture
implementation("androidx.room:room-guava:$room_version")
// optional - Test helpers
testImplementation("androidx.room:room-testing:$room_version")
}
기본 구성요소
Room에는 다음 3가지 주요 구성요소가 있습니다.
- 데이터베이스를 보유하고 앱의 영구 데이터와의 기본 연결을 위한 기본 액세스 포인트 역할을 하는 데이터베이스 클래스
- 앱 데이터베이스의 테이블을 나타내는 데이터 항목
- 앱이 데이터베이스의 데이터를 쿼리, 업데이트, 삽입, 삭제하는 데 사용할 수 있는 메서드를 제공하는 데이터 액세스 객체 (DAO)
그림 1은 다양한 Room 구성요소 간 관계를 보여줍니다.
샘플 구현
// Entity
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
// DAO
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}
// Database
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
// Usage
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
개체를 사용하여 데이터 정의
각 Room 항목은 데이터베이스의 테이블을 나타냅니다. 각 항목을
클래스로 정의합니다. @Entity
@Entity(tableName = "users")
data class User (
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?,
@Ignore val picture: Bitmap? = null
)
- 맞춤 테이블 및 열 이름: 기본적으로 Room은 클래스 이름을
테이블 이름으로, 속성 이름을 열 이름으로 사용합니다. 맞춤설정하려면
tableName속성을@Entity및@ColumnInfo(name = "...")주석에 사용합니다. - 기본 키: 기본 키를 정의하려면
@PrimaryKey를 사용합니다. 복합 키의 경우primaryKeys속성(@Entity)을 사용합니다.@Entity(primaryKeys = ["firstName", "lastName"]) - 필드 무시: 필드가 유지되지 않도록 하려면
@Ignore를 사용합니다.
유형 변환기
때로 Date와 같은 맞춤 유형을 단일 열에 저장해야 합니다.
Room이 유지할 수 있는 유형과 맞춤 유형을 상호 변환하는 @TypeConverter 메서드를 제공합니다.
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@TypeConverter
fun dateToTimestamp(date: Date?): Long? = date?.time
}
// Register in your Database class
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }
DAO를 사용하여 데이터 액세스
DAO는 데이터베이스 상호작용을 위한 메서드를 정의합니다. 인터페이스 또는 추상
클래스에 @Dao 주석을 답니다.
편의 메서드
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg users: User)
@Update
fun updateUsers(vararg users: User)
@Delete
fun deleteUsers(vararg users: User)
}
- 삽입: 삽입 메서드는 삽입된 행 ID를 나타내는
Long또는 삽입된 모든 행의 ID가 포함된List<Long>을 반환할 수 있습니다. - 업데이트 또는 삭제: 업데이트 또는 삭제 메서드는 영향을 받은 행 수를 나타내는
Int를 반환할 수 있습니다.
쿼리 메서드
SQL 문을 작성하려면 메서드에 @Query 주석을 답니다. Room은 컴파일 시간에 쿼리를 검증합니다.
@Dao
interface UserDao {
// Simple query
@Query("SELECT * FROM user")
fun loadAllUsers(): Array<User>
// Return a subset of columns using a POJO or tuple
@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List<NameTuple>
// Pass parameters
@Query("SELECT * FROM user WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): Array<User>
// Collection of parameters
@Query("SELECT * FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<User>
// Join tables
@Query("SELECT * FROM book INNER JOIN user ON user.id = book.user_id WHERE user.name = :userName")
fun findBooksBorrowedByName(userName: String): List<Book>
}
멀티매핑 반환 유형
Room 2.4 이상에서는 쿼리 메서드가 Map 유형을 사용하여 멀티매핑을 직접 반환할 수 있습니다.
@Query("SELECT * FROM user JOIN book ON user.id = book.user_id")
fun loadUserAndBookNames(): Map<User, List<Book>>
비동기 DAO 쿼리
UI가 정지되지 않도록 데이터베이스 쿼리는 기본 스레드에서 실행할 수 없습니다. 다음 통합 중 하나를 사용하여 쿼리를 비동기식으로 만듭니다.
Kotlin 코루틴 및 Flow
room-ktx 종속 항목이 필요합니다.
@Dao
interface UserDao {
// One-shot async query
@Insert
suspend fun insertUsers(vararg users: User)
// Observable query using Flow
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flow<User>
}
RxJava를 사용하는 Java
room-rxjava2 또는 room-rxjava3이 필요합니다.
@Dao
interface UserDao {
@Insert
fun insertUsers(users: List<User>): Completable
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flowable<User>
}
LiveData 및 Guava를 사용하는 Java
ListenableFuture에 room-guava가 필요합니다.
@Dao
interface UserDao {
// LiveData for observable queries
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): LiveData<User>
// Guava ListenableFuture for one-shot queries
@Insert
fun insertUsers(users: List<User>): ListenableFuture<Integer>
}
Room 2.x에서 관계 정의
UI 스레드에서 지연 로드를 방지하려면 항목 간에 직접 객체 참조를 사용할 수 없습니다. 대신 중간
데이터 클래스를 사용하여 @Relation을 사용하여 관계를 정의합니다.
일대일
각 사용자에게는 하나의 라이브러리만 있습니다.
@Entity
data class User(@PrimaryKey val userId: Long, val name: String)
@Entity
data class Library(@PrimaryKey val libraryId: Long, val userOwnerId: Long)
// Intermediate class
data class UserAndLibrary(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userOwnerId"
)
val library: Library
)
// DAO Query
@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>
일대다
각 사용자에게는 여러 재생목록이 있을 수 있습니다.
@Entity
data class Playlist(@PrimaryKey val playlistId: Long, val userCreatorId: Long)
data class UserWithPlaylists(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<Playlist>
)
다대다
재생목록에는 여러 노래가 있을 수 있으며 노래는 여러 재생목록에 있을 수 있습니다. 연결 테이블이 필요합니다.
@Entity
data class Song(@PrimaryKey val songId: Long, val songName: String)
@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(val playlistId: Long, val songId: Long)
data class PlaylistWithSongs(
@Embedded val playlist: Playlist,
@Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = Junction(PlaylistSongCrossRef::class)
)
val songs: List<Song>
)
중첩된 관계
사용자, 재생목록, 재생목록의 모든 노래를 쿼리합니다.
data class UserWithPlaylistsAndSongs(
@Embedded val user: User,
@Relation(
entity = Playlist::class,
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<PlaylistWithSongs> // Nesting PlaylistWithSongs
)
데이터베이스 관리
이 섹션에서는 데이터베이스 뷰, 데이터 미리 채우기, 데이터베이스 이전을 비롯한 Room 데이터베이스 관리의 다양한 측면을 다룹니다.
데이터베이스 뷰
복잡한 쿼리를 @DatabaseView 주석이 달린 클래스에 캡슐화합니다.
@DatabaseView("SELECT user.id, user.name, department.name AS departmentName FROM user INNER JOIN department ON user.departmentId = department.id")
data class UserDetail(val id: Long, val name: String, val departmentName: String)
// Register in Database class
@Database(entities = [User::class], views = [UserDetail::class], version = 1)
abstract class AppDatabase : RoomDatabase() { ... }
데이터베이스 미리 채우기
초기화 시 애셋 파일 또는 파일 시스템에서 데이터베이스를 채웁니다.
Room.databaseBuilder(appContext, AppDatabase::class.java, "Sample.db")
.createFromAsset("database/myapp.db")
.build()
이전
스키마를 변경할 때는 데이터베이스 버전을 늘리고
Migration 객체를 정의합니다.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE User ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name")
.addMigrations(MIGRATION_1_2)
.build()
- 자동 이전: Room 2.4.0 이상을 사용하는 경우
@AutoMigration을 사용하여 기본 스키마 변경사항을 자동으로 이전할 수 있습니다. 이렇게 하려면 데이터베이스 구성에서exportSchema를true로 설정해야 합니다.@Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]) - 파괴적 대체: 이전 경로
가 누락되었을 때 데이터 손실이 허용된다면 데이터베이스를 빌드할 때
.fallbackToDestructiveMigration를 호출합니다.
이전 테스트
이전을 확인하려면 room-testing 아티팩트의 MigrationTestHelper를 사용합니다. 이를 지원하려면 build.gradle 구성에서 스키마를 내보내야 합니다.
@RunWith(AndroidJUnit4::class)
class MigrationTest {
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
@Test
fun migrate1To2() {
var db = helper.createDatabase("test-db", 1).apply {
execSQL("INSERT INTO User VALUES (1, 'John')")
close()
}
db = helper.runMigrationsAndValidate("test-db", 2, true, MIGRATION_1_2)
// Verify data was migrated correctly
}
}
SQLite에서 Room으로 이전
SQLite에서 Room으로 앱을 이전하려면 다음 단계를 완료하세요.
- Room을 포함하도록 종속 항목을 업데이트 합니다.
@Entity,@PrimaryKey,@ColumnInfo로 모델 클래스에 주석을 답니다.- 도우미 쿼리 메서드를 대체할 DAO를 만듭니다.
- 항목과 DAO를 참조하는 RoomDatabase 클래스를 만듭니다. 버전 번호를 늘립니다.
- 스키마가 변경되지 않고 프레임워크만 변경되므로 빈 이전 경로를 정의합니다.
프레임워크:
kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} } - 이전 경로와 함께
Room.databaseBuilder를 사용하도록 인스턴스화를 업데이트 합니다.