Room 지속성 라이브러리를 사용하여 앱 데이터를 저장할 때 저장된 데이터와 상호작용 하려면 데이터 액세스 객체(DAO)를 정의해야 합니다. 각 DAO에는 앱 데이터베이스에 대한 추상 액세스를 제공하는 함수가 포함되어 있습니다. Room은 컴파일 시간에 정의된 DAO 구현을 자동으로 생성합니다.
쿼리 빌더나 직접 쿼리 대신 DAO를 사용하여 앱 데이터베이스에 액세스하면 중요한 아키텍처 원칙인 관심사 분리를 유지할 수 있습니다. DAO를 사용하면 앱을 테스트할 때 데이터베이스 액세스도 모의 처리할 수 있습니다. 앱을 테스트할 때
DAO 분석
각 DAO를 인터페이스나 추상 클래스로 정의할 수 있습니다. 기본 사용 사례에서는 일반적으로 인터페이스를 사용합니다. 어느 경우든 DAO에 항상
주석을 달아야 합니다@Dao. DAO에는 속성이 없지만 앱 데이터베이스의 데이터와 상호작용하는 함수를 하나 이상 정의합니다.
다음 코드는 Room 데이터베이스에서 User 객체를 삽입, 삭제, 선택하는 함수를 정의하는 DAO의 예입니다.
@Dao interface UserDao { @Insert suspend fun insertAll(vararg users: User) @Delete suspend fun delete(user: User) @Query("SELECT * FROM user") suspend fun getAll(): List<User> }
데이터베이스 상호작용을 정의하는 두 가지 유형의 DAO 함수가 있습니다.
- 편의 함수: SQL 코드 작성 없이 데이터베이스에서 행을 삽입하고 업데이트하고 삭제할 수 있습니다.
- 쿼리 함수: 자체 SQL 쿼리를 작성하여 데이터베이스와 상호작용할 수 있습니다.
다음 섹션에서는 두 가지 유형의 DAO 함수를 모두 사용하여 앱에 필요한 데이터베이스 상호작용을 정의하는 방법을 보여줍니다.
편의 함수
Room은 개발자가 SQL 문을 작성하지 않아도 삽입과 업데이트, 삭제를 실행하는 함수를 정의하는 편의 주석을 제공합니다.
좀 더 복잡한 삽입이나 업데이트, 삭제를 정의해야 하거나 데이터베이스의 데이터를 쿼리해야 하면 대신 쿼리 함수를 사용하세요.
삽입
@Insert 주석을 사용하면 데이터베이스의 적절한 테이블에
매개변수를 삽입하는 함수를 정의할 수 있습니다. 다음 코드는 데이터베이스에 User 객체를 하나 이상 삽입하는 유효한 @Insert 함수의 예를 보여줍니다.
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUsers(vararg users: User) @Insert suspend fun insertBothUsers(user1: User, user2: User) @Insert suspend fun insertUsersAndFriends(user: User, friends: List<User>) }
@Insert 함수의 각 매개변수는 Room
데이터 항목 클래스의 인스턴스이거나 @Entity 주석이 달린 데이터 항목
클래스 인스턴스의 컬렉션이어야 합니다. @Insert 함수가 호출되면 Room은 전달된 각 항목 인스턴스를 상응하는 데이터베이스 테이블에 삽입합니다.
@Insert 함수가 단일 매개변수를 수신하면 Long 값을 반환할 수 있고 이 값은 삽입된 항목의 새 rowId입니다. 매개변수가 배열이나 컬렉션이면 메서드는 대신 Long 값의 배열이나 컬렉션을 반환해야 하며 각 값은 삽입된 항목 중 하나의 rowId입니다.
rowId 값 반환에 관한 자세한 내용은 @Insert 주석 참조
문서와 rowid 테이블 SQLite 문서
를 참고하세요.
업데이트
@Update 주석을 사용하면 데이터베이스 테이블에서 특정
행을 업데이트하는 함수를 정의할 수 있습니다. @Insert 함수와 마찬가지로 @Update 함수는 데이터 항목 인스턴스를 매개변수로 허용합니다. 다음 코드는 데이터베이스에서 User 객체를 하나 이상 업데이트하려고 하는 @Update 함수의 예를 보여줍니다.
@Dao interface UserDao { @Update suspend fun updateUsers(vararg users: User) }
Room은 기본 키를 사용하여 인수에서 항목 인스턴스를 데이터베이스의 행과 일치시킵니다. 기본 키가 같은 행이 없으면 Room에서는 아무것도 변경하지 않습니다.
@Update 함수는 성공적으로 업데이트된 행 수를 나타내는 Int 값을 선택적으로 반환할 수 있습니다.
삭제
@Delete 주석을 사용하면 데이터베이스 테이블에서 특정 행을 삭제하는 함수를 정의할 수 있습니다. @Insert 함수와 마찬가지로 @Delete 함수는 데이터 항목 인스턴스를 매개변수로 허용합니다. 다음 코드는 데이터베이스에서 User 객체를 하나 이상 삭제하려고 하는 @Delete 함수의 예를 보여줍니다.
@Dao interface UserDao { @Delete suspend fun deleteUsers(vararg users: User) }
Room은 기본 키를 사용하여 인수에서 항목 인스턴스를 데이터베이스의 행과 일치시킵니다. 기본 키가 같은 행이 없으면 Room에서는 아무것도 변경하지 않습니다.
@Delete 함수는 성공적으로 삭제된 행 수를 나타내는 Int 값을 선택적으로 반환할 수 있습니다.
삽입/업데이트
@Upsert 주석을 사용하면 일치하는 행이 없는 경우 항목 인스턴스를 삽입하거나
기본 키가 같은 행이 이미 있는 경우 항목 인스턴스를 업데이트하는 함수를
정의할 수 있습니다.
@Insert 및 @Update 함수와 마찬가지로 @Upsert 함수는 데이터 항목 인스턴스를 매개변수로 허용합니다. 다음 코드는 데이터베이스에서 User 객체를 하나 이상 삽입/업데이트하려고 하는 @Upsert 함수의 예를 보여줍니다.
@Dao interface UserDao { @Upsert suspend fun upsertUsers(vararg users: User) }
@Upsert 함수가 단일 매개변수를 수신하면 Long 값을 반환할 수 있습니다. 새 행이 삽입되면 새로 삽입된 행의 rowId를 반환합니다. 기존 행이 업데이트되면 -1을 반환합니다. 매개변수가 배열이나 컬렉션이면 대신 Long 값의 배열이나 컬렉션을 반환해야 합니다.
쿼리 함수
@Query 주석을 사용하면 SQL 문을 작성하고 이를 DAO 함수로 노출할 수 있습니다. 이러한 쿼리 함수는 앱 데이터베이스에서 데이터를 쿼리하거나 좀 더 복잡한 삽입, 업데이트, 삭제를 실행해야 할 때 사용합니다.
Room은 컴파일 시간에 SQL 쿼리를 검증합니다. 즉, 쿼리에 문제가 있으면 런타임 실패가 아닌 컴파일 오류가 발생합니다.
간단한 쿼리
다음 코드는 SELECT 쿼리를 사용하여 데이터베이스의 모든 User 객체를 반환하는 함수를 정의합니다.
@Query("SELECT * FROM user") suspend fun loadAllUsers(): List<User>
다음 섹션에서는 일반적인 사용 사례에서 이 예를 수정하는 방법을 보여줍니다.
테이블 열의 하위 집합 반환
대부분의 경우 쿼리하는 테이블에서 열의 하위 집합만 반환하면 됩니다. 예를 들어 UI에는 사용자에 관한 모든 세부정보 대신 사용자의 성과 이름만 표시될 수 있습니다. 리소스 절약 및 쿼리 실행 간소화를 위해 필요한 속성만 쿼리하세요.
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") suspend fun loadFullName(): List<NameTuple>
쿼리는 first_name 및 last_name 열의 값을 반환하므로 Room은 이러한 값을 NameTuple 클래스의 속성에 매핑합니다. 쿼리가 반환된 객체의 속성에 매핑되지 않는 열을 반환하면 Room에서는 경고를 표시합니다.
이전 예에서는 맞춤 데이터 클래스를 사용하여 열의 하위 집합을 가져오지만 쿼리가 정확히 두 개 또는 세 개의 열을 반환할 때 편의를 위해 kotlin.Pair 및 kotlin.Triple 반환도 지원합니다. 이러한 유형을 사용할 때 열은 쿼리 문에 정의된 순서대로 매핑되므로 SELECT 문의 열 순서는 Pair 또는 Triple의 유형 순서와 일치해야 합니다.
쿼리에 단순 매개변수 전달
대부분의 경우 DAO 함수는 필터링 작업을 실행할 수 있도록 매개변수를 허용해야 합니다. Room은 쿼리에서 함수 매개변수를 결합 매개변수로 사용하는 것을 지원합니다.
예를 들어 다음 코드는 특정 연령 이상의 모든 사용자를 반환하는 함수를 정의합니다.
@Query("SELECT * FROM user WHERE age > :minAge") suspend fun loadAllUsersOlderThan(minAge: Int): Array<User>
다음 코드와 같이 쿼리에서 여러 매개변수를 전달하거나 같은 매개변수를 여러 번 참조할 수도 있습니다.
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge") suspend fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User> @Query( """ SELECT * FROM user WHERE first_name LIKE :search OR last_name LIKE :search """ ) suspend fun findUserWithName(search: String): List<User>
쿼리에 매개변수 컬렉션 전달
일부 DAO 함수에서는 런타임까지 알 수 없는 가변 개수의 매개변수를 전달해야 할 수 있습니다. 매개변수가 컬렉션을 나타내는 경우 값의 수에 따라 런타임에 자동으로 확장됩니다.
예를 들어 다음 코드는 일부 지역의 모든 사용자에 관한 정보를 반환하는 함수를 정의합니다.
@Query("SELECT * FROM user WHERE region IN (:regions)") suspend 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 """ ) suspend fun findBooksBorrowedByName(userName: String): List<Book>
데이터 객체를 정의하여 여러 조인된 테이블에서 열의 하위 집합을 반환할 수도 있습니다. 자세한 내용은 테이블 열의 하위 집합 반환을 참고하세요. 다음 코드는 사용자의 이름과 대출한 도서의 이름을 반환하는 함수가 있는 DAO를 정의합니다.
interface UserBookDao { @Query( """ SELECT user.name AS userName, book.name AS bookName FROM user, book WHERE user.id = book.user_id """ ) fun loadUserAndBookNames(): Flow<List<UserBook>> } data class UserBook(val userName: String, val bookName: String)
멀티매핑 반환
조인 작업의 경우 멀티매핑을 반환하는 쿼리 함수를 작성하여 추가 데이터 클래스를 정의하지 않고도 여러 테이블의 열을 쿼리할 수도 있습니다.
여러 테이블 쿼리의 예를 참고하세요. User 인스턴스와 Book 인스턴스의 쌍을 보유하는 맞춤 데이터 클래스의 인스턴스 목록을 반환하는 대신 쿼리 함수에서 User 및 Book의 매핑을 직접 반환할 수 있습니다.
@Query( """ SELECT * FROM user JOIN book ON user.id = book.user_id """ ) suspend fun loadUserAndBookNames(): Map<User, List<Book>>
쿼리 함수가 멀티매핑을 반환하면 GROUP BY 절을 사용하는 쿼리를 작성하여 고급 계산 및 필터링을 위한 SQL의 기능을 활용할 수 있습니다. 예를 들어 대출된 도서가 3권 이상인 사용자만 반환하도록 loadUserAndBookNames 함수를 수정할 수 있습니다.
@Query( """ SELECT * FROM user JOIN book ON user.id = book.user_id GROUP BY user.name HAVING COUNT(book.id) >= 3 """ ) suspend fun loadUserAndBookNamesGrouped(): Map<User, List<Book>>
전체 객체를 매핑할 필요가 없는 경우 반환 유형의 일반 매개변수에 @MapColumn 주석을 사용하여 쿼리의 특정 열 간 매핑을 반환할 수도 있습니다.
@Query( """ SELECT user.name AS username, book.name AS bookname FROM user JOIN book ON user.id = book.user_id """ ) suspend fun loadUserAndBookNamesColumns(): Map< @MapColumn(columnName = "username") String, List<@MapColumn(columnName = "bookname") String> >
특수 반환 유형
Room은 다른 API 라이브러리와의 통합을 위해 특수 반환 유형을 제공합니다.
를 참고하세요.Paging 라이브러리를 사용하여 페이지로 나눈 쿼리
Room은 Paging 라이브러리와의 통합을 통해 페이지로 나눈 쿼리를 지원합니다. Paging 3 반환 유형을 사용하려면 데이터베이스 또는 DAO에 Paging 반환 유형 변환기를 등록해야 합니다.
- 빌드 구성에
androidx.room3:room3-paging아티팩트를 포함합니다. @Database또는@Dao선언에@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)주석을 답니다.
등록되면 DAO는 PagingSource 객체를
Paging 3과 함께 사용할 수 있습니다.
@Dao @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) interface UserDao { @Query("SELECT * FROM users WHERE label LIKE :query") fun pagingSource(query: String): PagingSource<Int, User> }
PagingSource의 유형 매개변수 선택에 관한 자세한 내용은
키 및 값 유형 선택을 참고하세요.
직접 데이터베이스 연결 액세스
앱의 로직에 데이터베이스 연결에 대한 직접적인 하위 수준 액세스가 필요한 경우 대신 Room의 연결 API를 사용할 수 있습니다. RoomDatabase 인스턴스에서 읽기 전용 작업에 useReaderConnection을 사용하거나 쓰기 작업에 useWriterConnection을 사용하여 연결을 가져오고 usePrepared을 사용하여 문을 실행할 수 있습니다.
val result: List<Pair<Long, String>> = roomDatabase.useReaderConnection { connection -> connection.usePrepared( "SELECT * FROM user WHERE age > :minAge LIMIT 5" ) { stmt -> // Bind arguments if needed stmt.bindLong(1, minAge.toLong()) buildList { // Step through the results while (stmt.step()) { add(stmt.getLong(0) to stmt.getText(1)) } } } }
연결에서 직접 하위 수준 데이터베이스 트랜잭션을 실행해야 하는 경우
useWriterConnection 블록 내의 Transactor 인스턴스에서 immediateTransaction,
deferredTransaction, 또는 exclusiveTransaction 도우미 함수를 사용할 수 있습니다:
roomDatabase.useWriterConnection { transactor -> transactor.immediateTransaction { // Perform transactional database operations using transactor } }
또는 트랜잭션에서 상위 수준 DAO 작업만 실행해야 하는 경우 withReadTransaction 또는 withWriteTransaction
도우미 확장 함수를 RoomDatabase 인스턴스에서 사용합니다.
// Perform transactional read operations (DEFERRED transaction) val userCount = roomDatabase.withReadTransaction { userDao.countUsers() } // Perform transactional write operations (IMMEDIATE transaction) roomDatabase.withWriteTransaction { userDao.insert(newUser) userDao.update(existingUser) }