Entity
앱에 데이터를 저장할 때는 저장하려는 객체를 나타내는 Entity를 정의해야 한다.
각 Entity는 연결된 Room 데이터베이스의 테이블에 상응한다.
Entity의 각 인스턴스는 상응하는 테이블의 데이터 행 하나를 나타낸다.
즉, SQL 코드를 작성하지 않고도 Room Entity를 사용하여 데이터베이스 스키마를 정의할 수 있다.
Entity 구조
각 Room Entity는 @Entity 애노테이션이 달린 클래스로 정의한다.
Room Entity에는 기본 키(Primary Key)를 구성하는 하나 이상의 columns 비롯하여 데이터베이스의 상응하는 테이블에 있는 각 columns의 필드가 포함되어 있다.
필드를
다음은 간단히 User 테이블을 정의하는 예시이다.
@Entity
data class User(
@PrimaryKey val id: Int,
val firstName: String?,
val lastName: String?
)
기본적으로 Room은 클래스 이름을 테이블 이름으로 사용한다.
테이블 이름을 다르게 하려면 @Entity 애노테이션의 tableName 속성을 설정하면 된다.
Room은 기본적으로 필드 이름을 column 이름으로 사용한다.
column 이름을 다르게 하려면 @ColumnInfo 애노테이션을 필드에 추가하고 name 속성을 설정하면 된다.
참고로 SQLite의 테이블 및 column 이름은 대소문자를 구분하지 않는다.
@Entity(tableName = "users")
data class User (
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
기본 키(Primary Key) 정의
각 Room Entity는 상응하는 데이터베이스 테이블의 각 column을 고유하게 식별하는 기본 키(Primary Key)를 정의해야 한다.
가장 간단한 방법은 @PrimaryKey 애노테이션을 다는 것이다.
자동 ID를 할당하게 하려면 @PrimaryKey 의 autoGenerate 속성을 true로 설정하면 된다.
@PrimaryKey val id: Int
복합 기본 키 정의
Entity 인스턴스가 여러 열의 조합으로 고유하게 식별되도록 하려면 이러한 열을 @Entity의 primaryKeys 속성에 나열하여 복합 기본 키를 정의하면 된다.
@Entity(primaryKeys = ["firstName", "lastName"])
data class User(
val firstName: String?,
val lastName: String?
)
필드 무시
기본적으로 Room은 Entity에 정의된 각 필드의 column을 생성한다.
Entity의 필드를 유지하지 않으려면 @Ignore 애노테이션을 사용하여 필드에 주석을 달면 된다.
@Entity
data class User(
@PrimaryKey val id: Int,
val firstName: String?,
val lastName: String?,
@Ignore val picture: Bitmap?
)
Entity가 상위 Entity에서 필드를 상속하는 경우 일반적으로 @Entity 속성의 ignoredColumns 속성을 사용하는 것이 더 쉽다.
open class User {
var picture: Bitmap? = null
}
@Entity(ignoredColumns = ["picture"])
data class RemoteUser(
@PrimaryKey val id: Int,
val hasVpn: Boolean
) : User()
테이블 검색 지원 제공
Room은 데이터베이스 테이블에서 세부정보를 더 쉽게 검색할 수 있게 하는 여러 유형의 애노테이션을 지원한다.
minSdkVersion이 16 이상이라면 전체 텍스트 검색(FTS)을 지원한다.
전체 텍스트 검색 지원
앱에서 전체 텍스트 검색을 통해 데이터베이스 정보에 매우 빠르게 액세스해야 한다면 FTS3 또는 FTS4 SQLite 확장 모듈을 사용하는 가상 테이블로 Entity를 지원해야 한다. 이 기능은 Room 2.1.0 이상에서 제공된다.
FTS 지원 테이블은 항상 INTEGER 유형의 기본 키와 'rowid'라는 column 이름을 사용한다.
FTS 테이블 지원 Entity에서 기본 키를 정의하는 경우 반드시 이러한 유형(INTEGER) 및 column 이름(rowid)을 사용해야 한다.
// Use `@Fts3` only if your app has strict disk space requirements or if you
// require compatibility with an older SQLite version.
@Fts4
@Entity(tableName = "users")
data class User(
/* Specifying a primary key for an FTS-table-backed entity is optional, but
if you include one, it must use this type and column name. */
@PrimaryKey @ColumnInfo(name = "rowid") val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?
)
특정 column index 생성
앱에서 FTS3 또는 FTS4 테이블 지원 Entity를 사용할 수 없는 SDK 버전을 지원해야하는 경우에도 데이터베이스에 있는 특정 column의 index를 생성하여 쿼리 속도를 높일 수 있다. Entity에 index를 추가하려면 @Entity 애노테이션 내에 indices 속성을 포함하여 index 또는 복합 index에 포함하려는 column의 이름을 나열하면 된다.
@Entity(indices = [Index(value = ["last_name", "address"])])
data class User(
@PrimaryKey val id: Int,
val firstName: String?,
val address: String?,
@ColumnInfo(name = "last_name") val lastName: String?,
@Ignore val picture: Bitmap?
)
특정 필드 또는 필드 그룹이 고유해야하는 경우도 있다.
@Index 애노테이션의 unique 속성을 true로 설정하여 고유 속성으로 만들 수 있다.
@Entity(indices = [Index(value = ["first_name", "last_name"],
unique = true)])
data class User(
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?,
@Ignore var picture: Bitmap?
)
데이터 액세스 객체 (DAO)
Room 지속성 라이브러리를 사용하여 앱 데이터를 저장할 때 데이터 액세스 객체(DAO)를 정의하여 저장된 데이터와 상호작용한다. 각 DAO에는 앱 데이터베이스에 관한 추상 액세스 권한을 제공하는 메서드가 포함되어 있다.
Room은 컴파일 시간에 정의된 DAO 구현을 자동으로 생성한다.
DAO를 사용하여 앱 데이터베이스에 액세스하면 중요한 아키텍처 원칙인 관심사 분리를 유지할 수 있다.
DAO 분석
각 DAO를 인터페이스나 추상 클래스로 정의할 수 있다. 일반적으로 인터페이스를 사용한다.
어느 경우든 DAO에는 항상 @DAO 애노테이션을 달아야 한다.
DAO에는 속성이 없지만 데이터베이스의 데이터와 상호작용하는 메서드를 하나 이상 정의한다.
데이터베이스 상호작용을 정의하는 DAO 메서드에는 두가지 유형이 있다.
편의 메서드
- SQL 코드 작성 없이 데이터베이스에서 행을 삽입하고 업데이트하고 삭제할 수 있다.(INSERT, UPDATE, DELETE)
쿼리 메서드
- 자체 SQL 쿼리를 작성하여 데이터베이스와 상호작용할 수 있다.
다음 코드는 Room 데이터베이스에서 User 객체를 삽입, 삭제, 조회하는 메서드를 정의하는 간단한 DAO 예이다.
@Dao
interface UserDao {
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
@Query("SELECT * FROM user")
fun getAll(): List<User>
}
편의 메서드
Room은 개발자가 SQL문을 작성하지 않아도 간단한 삽입, 업데이트, 삭제를 실행하는 메서드를 정의한 편의 애노테이션을 제공한다. 좀 더 복잡한 SQL이 필요하다면 쿼리 메서드를 사용하면 된다.
Insert 삽입
@Insert 애노테이션을 사용하면 데이터베이스의 적절한 테이블에 매개변수를 삽입하는 메서드를 정의할 수 있다.
@Insert 메서드의 각 매개변수는 @Entity 애노테이션이 달린 클래스의 인스턴스 이거나 @Entity 애노테이션이 달린 클래스 인스턴스의 컬렉션이어야 한다.
@Insert 메서드가 호출되면 Room은 전달된 각 Entity 인스턴스를 상응하는 테이블에 삽입(Insert)한다.
@Insert 메서드가 단일 매개변수를 수신하면 long 값을 반환할 수 있고 이 값은 삽입된 Entity의 새 rowId 이다.
매개변수가 배열이나 컬렉션이면 메서드는 long값의 배열이나 컬렉션을 반환해야 하며 각 값은 삽입된 Entity 들의 rowId이다.
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg users: User)
@Insert
fun insertBothUsers(user1: User, user2: User)
@Insert
fun insertUsersAndFriends(user: User, friends: List<User>)
}
Update 업데이트
@Update 애노테이션을 사용하면 특정 행을 업데이트하는 메서드를 실행할 수 있다.
@Insert 와 마찬가지로 매개변수로 Entity 인스턴스를 허용한다.
Room은 기본 키를 사용하여 전달된 Entity 인스턴스를 데이터베이스의 행과 일치시킨다.
기본 키가 같은 행이 없으면 Room에서는 아무것도 변경하지 않는다.
@Update 메서드는 성공적으로 업데이트 된 행 수를 나타내는 Int 값을 선택적으로 반환한다.
@Dao
interface UserDao {
@Update
fun updateUsers(vararg users: User)
}
Delete 삭제
@Delete 애노테이션을 사용하면 특정 행을 삭제하는 메서드를 실행할 수 있다.
@Insert 와 마찬가지로 매개변수로 Entity 인스턴스를 허용한다.
Room은 기본키를 사용하여 전달된 Entity 인스턴스를 데이터베이스의 행과 일치시킨다.
기본 키가 같은 행이 없으면 Room에서는 아무것도 변경하지 않는다.
@Delete 메서드는 성공적으로 삭제된 행 수를 나타내는 Int를 값을 선택적으로 반환한다.
@Dao
interface UserDao {
@Delete
fun deleteUsers(vararg users: User)
}
쿼리 메서드
@Query 애노테이션을 사용하면 SQL 문을 작성하여 DAO 메서드로 노출할 수 있다.
Room은 컴파일 시간에 SQL 쿼리를 검증합니다. 즉, 쿼리에 문제가 있으면 런타임 실패가 아닌 컴파일 오류가 발생합니다.
단순한 쿼리
@Query("SELECT * FROM user")
fun loadAllUsers(): Array<User>
테이블 열의 하위 집합 반환
대부분의 경우 쿼리하는 테이블에서 열의 하위 집합만 반환하면 된다.
리소스를 절약하고 쿼리 실행을 간소화하려면 필요한 필드만 쿼리해야 한다.
Room을 사용하면 결과 열 세트를 반환된 객체에 매핑할 수 있는 한 모든 쿼리에서 간단한 객체를 반환할 수 있다.
Room은 쿼리에서 first_name 과 last_name 열의 값을 반환하고 이러한 값을 NameTuple 클래스의 필드에 매핑될 수 있다는 것을 인식한다. 쿼리가 반환된 객체의 필드에 매핑되지 않는 열을 반환하면 Room에서는 경고를 표시한다.
data class NameTuple(
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List<NameTuple>
쿼리에 단순 매개변수 전달
대부분의 경우 DAO 메서드는 필터링 작업을 실행할 수 있도록 매개변수를 허용해야 한다.
Room은 쿼리에서 메서드 매개변수를 결합 매개변수로 사용하는 것을 지원한다.
@Query("SELECT * FROM user WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): Array<User>
쿼리에서 여러 매개변수를 전달하거나 같은 매개변수를 여러 번 참조할 수도 있다.
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>
@Query("SELECT * FROM user WHERE first_name LIKE :search " +
"OR last_name LIKE :search")
fun findUserWithName(search: String): List<User>
쿼리에 매개변수 컬렉션 전달
일부 DAO 메서드를 사용하면 런타임까지 알 수 없는 가변적인 수의 매개변수를 전달해야 할 수 있다.
Room은 매개변수가 언제 컬렉션을 나타내는지 인식하고 제공된 매개변수 수에 따라 런타임 시에 자동으로 확장한다.
@Query("SELECT * FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<User>
여러 테이블 쿼리
일부 쿼리는 결과를 계산하기 위해 여러 테이블에 액세스해야 할 수 있다.
SQL 쿼리에서 JOIN 절을 사용하여 테이블을 두 개 이상 참조할 수 있다.
@Query(
"SELECT * FROM book " +
"INNER JOIN loan ON loan.book_id = book.id " +
"INNER JOIN user ON user.id = loan.user_id " +
"WHERE user.name LIKE :userName"
)
fun findBooksBorrowedByNameSync(userName: String): List<Book>
데이터베이스
데이터베이스를 보유할 클래스를 정의한다.
이 클래스는 데이터베이스 구성을 정의하고 영구 데이터에 대한 앱의 기본 액세스 포인트 역할을 한다.
데이터베이스 클래스는 다음 조건을 충족해야 한다.
- 클래스에는 데이터베이스와 연결된 데이터 Entity를 모두 나열하는 entities 배열이 포함된 @Database 애노테이션이 달려야한다.
- 클래스는 RoomDatabase를 확장하는 추상 클래스여야 한다.
- 데이터베이스 클래스는 인수가 0개인 데이터베이스와 연결된 각 DAO 클래스의 인스턴스를 반환하는 추상 메서드를 정의해야 한다.
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
앱이 단일 프로레스에서 실행되면 데이터베이스 클래스 객체를 인스턴스화할 때 싱글톤 디자인 패턴을 따라야 한다.
각 RoomDatabase 인스턴스는 리소스를 상당히 많이 소비하며 단일 프로세스내에서 여러 인스턴스에 액세스해야 하는 경우는 거의 없다.
사용 방법
데이터 Entity와 DAO, 데이터베이스 객체를 정의한 후에는 다음 코드를 사용하여 데이터베이스 인턴스를 만들 수 있다.
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name" // 데이터베이스 클래스
).build()
데이터베이스 클래스의 추상 메서드를 사용하여 DAO 인스턴스를 가져올 수 있다.
결과적으로 DAO 인스턴스의 메서드를 사용하여 데이터베이스와 상호작용할 수 있다.
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
Reference
Room : https://developer.android.com/training/data-storage/room?hl=ko#kts
Room Entity : https://developer.android.com/training/data-storage/room/defining-data#ignore
Room DAO : https://developer.android.com/training/data-storage/room/accessing-data?hl=ko