본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[android] Accessing data using Room DAOs - Room 에 대해 알아보자

by 돼지왕 왕돼지 2020. 8. 18.
반응형

 

android room tutorial

 

-

Dao 는 Room 의 메인 컴퍼넌트로, 각각의 DAO 는 app db 에 접근하는 추상적인 방법을 제공한다.

직접 query 하거나 queryBuilder 를 통해 data 에 접근하는 대신, 각각의 컴퍼넌트에 대해 분리된 접근을 할 수 있다.

게다가 DAO 는 test 를 위한 mock db access 를 제공하기가 쉬워진다.

 

 

-

DAO 는 interface 나 abstract class 가 될 수 있다.

abstract class 라면, constructor 를 통해서 RoomDatabase 를 유일한 param 으로 받을 수 있다.

Room 은 각각의 DAO 를 compile time 에 생성한다.

 

 

-

Room 은 builder 에서 allowMainThreadQueries() 를 호출하기 전까지 main thread 에서의 db 접근을 허용하지 않는다. LiveData 나 Flowable 등을 return 하는 async query 는 이 규칙에서 예외인데, 그 이유는 그들은 원하면 bg thread 로 쉽게 작동시킬 수 있기 때문이다.

 

 

 

 

Define methods for convenience

 

* Insert

 

-

DAO 에 @Insert 로 annotate 하면, Room 은 single transaction 으로 모든 param 을 insert 하는 impl 을 만들어낸다.

@Dao
interface MyDao{
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUsers(vararg users: User)

    @Insert
    fun insertBothUsers(user1:User, user2:User)

    @Insert
    fun insertUsersAndFriends(user:User, friends:List<User>)
}

 

 

-

만약 @Insert 가 1 개의 param 만 받는다면, 새로운 rowId 인 long 을 return 한다.

만약 param 이 array 나 collection 이라면, long[] 이나 List<Long> 을 return 한다.

 

 

 

* Update

 

-

Update 는 param 으로 주어진 것을 수정한다.

match 는 각각의 primary key 를 기준으로 한다.

@Dao
interface MyDao{
    @Update
    fun updateUsers(vararg users:User)
}

 

보통 필요하지는 않지만, affected rows 에 해당 int 를 return 한다.

 

 

 

* Delete

 

-

primary key 비교를 통해 삭제한다.

@Dao
interface MyDao{
    @Delete
    fun deleteUsers(vararg users:User)
}

 

보통 필요하지는 않지만, 삭제된 row count 를 return 한다.

 

 

 

 

Query for information

 

-

@Query 는 DAO class 의 핵심 annotation 이다.

이를 통해 db 를 읽거나 db 에 쓰는 작업을 할 수 있다.

각각의 @Query 함수는 compile time 에 verify 된다. 만약 query 에 문제가 있다면 compile error 가 난다. (runtime 이 아니다!!)

 

 

-

Room 은 query 의 return value 도 검증한다.

그래서 만약 field name 이 일치하지 않거나 한다면, Room 은 두가지 방법으로 경고를 준다.

1. 몇개의 field name 만 match 하면 warning

2. 모든 field name 이 match 하지 않으면 error

 

 

* Simple queries

 

-

@Dao
interface MyDao{
    @Query("SELECT * FROM user")
    fun loadAllUsers(): Array<User>
}

 

컴파일타임에 Room 은 모든 user table 의 모든 column 을 안다.

그래서 query 가 syntax error 가 있거나, user table 이 db 에 없거나 하는 등의 error 가 있다면 app compile 타입에 에러를 보여준다.

 

 

 

* Passing parameters into the query

 

-

param 으로 filtering 관련된 정보를 제공할 수 있다. 예를 들면 특정 나이 이상의 user 만 query 하라는..

@Dao
interface MyDao{
    @Query("SELECT * FROM user WHERE age > :minAge")
    fun loadAllUsersOlderThan(minAge: Int): Array<User>
}

 

컴파일시에, Room 은 :minAge 를 찾아 param bind 를 수행한다.

 

 

-

여러개의 param 도 한번에 활용할 수 있다.

interface MyDao{
    @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>
}

 

 

 

* Returning subsets of columns

 

-

대부분의 경우 entity 의 몇개의 field 정보만 채용하고 싶을 것이다.

예를 들면 UI 가 user 의 모든 정보 대신, 성과 이름만을 표현한다던지 하는 케이스이다.

관심있는 column 만 query 함으로써 더 빠르게 결과를 얻을 수도 있고, res 도 아낄 수 있다.

 

 

-

Room 은 query result 로 column matching 만 잘 된다면, 어떤 Java-based object 든 return 할 수 있게 해준다.  예를 들어 성과 이름만 query 결과로 전달하고 싶다면 아래와 같이 하면 된다.

data class NameTuple(
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?
)

 

그리고 DAO 의 query 는 아래와 같이 하면 된다.

@Dao
interface MyDao{
    @Query("SELECT first_name, last_name FROM user")
    fun loadFullName(): List<NameTuple>
}

 

Room 은 first_name 과 last_name column 을 NameTuple 안의 field 와 mapping 해서 문제가 있다면 compile error 를 던진다.

만약 query 가 return Model 이 가진 field 보다 더 많은 column 을 return 한다면 warning 을 보여준다.

( 이 POJO 는 @Embedded annotation 을 쓸 수도 있다. )

 

 

 

* Passing a collection of arguments

 

-

특정 지역”들"에 있는 user 들을 query 하는 등의 여러 개의 param list(array) 를 동시에 처리해야 하는 경우가 있다.

@Dao
interface MyDao{
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    fun loadUsersFromRegions(regions: List<String>): List<NameTuple>
}

 

 

 

* Observable queries

 

-

query 를 할 때 UI 가 data 변화에 자동으로 반응하게 하고 싶을 때가 있다.

이를 위해서 LiveData(또는 Flow) 를 return 할 수 있다.

Room 은 LiveData 관련된 모든 코드를 자동으로 생성해서 제공한다.

@Dao
interface MyDao{
    @Query("SELECT first_name, last_name FROM user WHERE region in (:regions)")
    fun loadUsersFromRegionsSync(regions: List<String>): LiveData<List<User>>
}

 

버전 1.0 부터 Room 은 LiveData update 를 판단하기 위해 query 에 명시된 table list 를 이용한다.

 

 

 

* Reactive queries with RxJava

 

-

RxJava2 을 지원한다.

@Query : Room 은 Publisher, Flowable, Observable 의 return 을 지원한다.

@Insert, @Update, @Delete : Room 2.1.0 이상부터 Completable, Single<T>, Maybe<T> 의 return 을 지원한다.

 

 

-

rxjava2 관련 기능을 사용하려면 다음을 dependency 로 추가해야 한다.

dependencies{
    implementation ‘androidx.room:room-rxjava2:2.1.0-beta01’ // version 은 알아서
}

 

 

-

@Dao
interface MyDao{
    @Query("SELECT * FROM user WHERE id = :id LIMIT 1")
    fun loadUserById(id: Int) : Flowable<User>

    @Insert
    fun insertLargeNumberOfUsers(users: List<User>): Maybe<Int>

    @Insert // operation 이 성공적으로 끝남을 확신해야 한다.
    fun insertLargeNumberOfUsers(varargs users: User): Completable

    @Delete // 적어도 한명은 무조건 삭제됨을 보장해야 한다.
    fun deleteAllUsers(users: List<User>):Single<Int>
}

 

 

 

* Direct cursor access

 

-

@Dao
interface MyDao{
    @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
    fun loadRawUsersOlderThan(minAge: Int): Cursor
}

 

Cursor api 를 사용하는것은 강비추이다.

이것을 쓰는 케이스는 이미 cursor 를 요구하는 코드와의 호환성 이슈로 cursor 를 제공하는 경우를 제외하고는 쓰지 않는 것이 좋다.

 

 

 

 

* Querying multiple tables

 

-

몇몇 query 는 결과를 내기 위해 여러개의 table 에 접근할 수 있다.

Room 은 table join 을 허용한다.

게다가 결과값이 Flowable 이나 Livedata 같은 observable data type 일 경우, Room 은 관련된 모든 table 들을 관찰한다.

 

 

-

아래는 책을 빌리는 유저와 대여상태인 책에 대한 정보를 통합하는 것을 보여준다.

@Dao
interface MyDao{
    @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>
}

 

 

-

@Dao
interface MyDao{
    @Query(
        """SELECT user.name AS userName, pet.name as petName 
            FROM user, pet
            WHERE user.id = pet.user_id
        """)
    fun loadUserAndPetNames(): LiveData<List<UserPet>>

    data class UserPet(val userName:String?, val petName:String?)
}

 

 

 

* Write async methods with Kotlin coroutines

 

-

DAO method 에 suspend kotlin keyword 를 써서 kotlin coroutine 의 async 로 만들 수 있다.

이것이 main thread 에서 불릴 수 없음을 확인시켜준다.

@Dao
interface MyDao{
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsers(vararg users:User)

    @Update
    suspend fun updateUsers(vararg users: User)

    @Delete
    suspend fun deleteUsers(vararg users: User)


    @Query("SELECT * FROM user")
    suspend fun loadAllUsers(): Array<User>
}

 

 

-

Room 을 kotlin coroutine 이랑 함께 사용하려면 Room 2.1 이상, Kotlin 1.3 이상, Couroutine 1.0 이상의 조건이 만족되어야 한다.

 

 

-

@Transaction 이 annotate 된 DAO 함수들에도 이것이 적용될 수 있다.

@Dao
abstract class UsersDao{
    @Transaction
    open suspend fun setLoggedInUser(loggedInUser: User){
        deleteUser(loggedInUser)
        insertUser(loggedInUser)
    }

    @Query("DELETE FROM users")
    abstract fun deleteUser(user: User)

    @Insert
    abstract suspend fun insertUser(user: User)
}

 

 

-

@Transaction 을 마킹하면 앱 사이드에서 따로 하는 것을 피하는 것이 좋다.

 

 

-

참고자료

https://developer.android.com/training/data-storage/room/accessing-data

 

 

반응형

댓글