안드로이드) Room (2)

밍나·2022년 6월 2일
0

Android

목록 보기
36/36

✏️ DAO를 사용한 데이터 접근

DAO를 사용한 데이터 접근

  • DAO는 앱의 데이터베이스에 접근하기 위한 추상 메서드(삽입, 삭제, 수정 등)를 포함하는 인터페이스이다.
  • 쿼리 빌더나 직접적인 쿼리 대신 DAO 클래스를 사용하면 데이터베이스 구성 요소를 분리하고, 단위 테스트를 수월하게 한다.
import androidx.room.*

@Dao
interface UserDao {
    @Insert
    fun setInsertUser(user: User)

    @Update
    fun setUpdateUser(user: User)

    @Delete
    fun setDeleteUser(user: User)

    @Query("SELECT * FROM User")
    fun getUserAll(): List<User>
}
  • @Insert, @Update, @Delete 어노테이션은 각각 삽입, 수정, 삭제로 쿼리를 작성하지 않고 간편하게 데이터를 처리할 수 있다. 이 때 함수의 매개변수는 위에서 정의한 Entity를 사용해야 한다.
  • 데이터베이스 조회(Select)나 복잡한 쿼리를 사용해야 할 때는 @Query 어노테이션을 사용해 직접 쿼리를 작성한다.

✏️ 삽입, 수정, 삭제 메서드 정의하기

삽입하기

@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>)
}
  • @Insert 어노테이션을 추가한 DAO 메서드를 작성하면 Room은 모든 매개 변수를 단일 트랜잭션으로 데이터베이스에 삽입하는 구현체를 컴파일 타임에 생성한다.
  • 만약 @Insert 메서드가 하나의 매개 변수를 받는다면, 메서드는 삽입되는 아이템의 long형 id를 반환할 수도 있다.
  • 만약 매개 변수가 배열 또는 컬렉션이라면 long[] 또는 List<Long>을 반환한다.

수정하기

@Dao
interface UserDao {
    @Update
    fun updateUsers(vararg users: User)
}
  • @Update 어노테이션을 추가한 DAO 메서드는 주어진 매개 변수로부터 데이터베이스의 엔티티들을 수정할 수 있다.
  • 내부적으로 각 엔티티의 기본키에 해당하는 내용을 수정하는 쿼리를 사용한다.
  • 일반적으로 필요 없지만 이 메서드는 데이터베이스 내에서 수정되는 행의 개수인 int 값을 반환할 수도 있다.

삭제하기

@Dao
interface UserDao {
    @Delete
    fun deleteUsers(vararg users: User)
}
  • @Delete 어노테이션을 추가한 DAO 메서드는 주어진 매개 변수로부터 데이터베이스 내의 엔티티들을 삭제한다.
  • 내부적으로 각 엔티티의 기본키에 해당하는 내용을 삭제하는 쿼리를 사용한다.
  • Update와 마찬가지로 데이터베이스 내에서 삭제되는 행의 개수인 int 값을 반환할 수도 있다.

✏️ 쿼리하기

  • @Query 어노테이션은 DAO 클래스에서 사용되는 주된 어노테이션으로 데이터베이스에서 읽기/쓰기 작업을 수행한다.

간단한 쿼리

@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun loadAllUsers(): Array<User>
}
  • 위의 예제는 모든 User를 로드하는 간단한 쿼리다.
  • 컴파일 시 Room은 user 테이블의 모든 컬럼을 쿼리한다.

쿼리에 매개 변수 전달

@Query("SELECT * FROM user WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): Array<User>
  • 쿼리 시 필터링 작업을 수행하려면 매개 변수를 쿼리에 전달해야 한다.
  • Room은 @Query 속성의 매개 변수명과 DAO 메서드의 매개 변수명이 일치하는 경우 컴파일 타임에 바인딩하여 처리한다. 일치되지 않는 이름은 컴파일 타임에 오류가 발생한다.
    • 위의 예제에서는 :minAge와 minAge의 이름을 일치시켜 메서드의 매개 변수를 쿼리의 매개 변수로 전달하여 처리한다.
@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>
  • 위의 예제처럼 쿼리에서 여러 매개 변수를 전달하거나 하나의 매개 변수를 여러 번 참조할 수도 있다.

테이블 내 칼럼의 일부만 반환하기

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>
  • 엔티티의 모든 정보가 아닌 일부 필드만 가져와야 하는 경우, Room을 사용하면 컬럼의 일부만 매핑하는 객체를 반환하는 DAO 쿼리 메서드를 만들 수 있다.
  • 위의 예제는 사용자의 firstName과 lastName만 가져온 예제로, Room은 쿼리가 first_name 및 last_name 칼럼값을 반환하며, 이러한 값을 Name Tuple 클래스의 필드에 매핑할 수 있다.

컬렉션을 매개 변수로 전달하기

@Query("SELECT * FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<User>
  • 특정 지역에 사는 사용자를 조회하는 경우 등 런타임 전까지 정확한 매개 변수를 알 수 없는 경우 컬렉션을 매개 변수로 전달한다.
  • 컬렉션을 매개 변수로 전달하면 Room은 이를 이해하고 제공된 매개 변수의 수에 따라 런타임에 자동으로 확장한다.

Observable 쿼리하기

@Query("SELECT * FROM user WHERE region In (:regions)")
fun loadUsers(regions: List<String>): LiveData<List<User>>
  • 쿼리 결과가 변경될 때마다 UI도 자동으로 업데이트 되도록 하기 위해서는 쿼리 메서드 반환형으로 LiveData를 사용한다.
  • Room은 컴파일 타임에 데이터베이스가 변경될 때 LiveData를 갱신하는 데 필요한 모든 코드를 생성한다.

RxJava로 반응형 쿼리 만들기

  • Room은 다음과 같은 RxJava 타입의 반환값을 지원한다.
    • @Query 메서드 : Roomdms Publisher, Flowable 및 Observable 타입의 반환값을 지원한다.
    • @Insert, @Update, @Delete 메서드 : Completable, Single<T> 및 Maybe<T> 타입의 반환값을 지원한다.
@Query("SELECT * FROM user WHERE id = :id LIMIT 1")
fun loadUserById(id: Int): Flowable<User>

// 데이터베이스에 추가된 사용자의 수를 발행한다.
@Insert
fun insertUsers(users: List<User>): Maybe<Integer>

// 작업이 성공적으로 끝났는지 확인한다.
@Insert
fun insertLargeNumberOfUsers(vararg users: User): Completable

// 데이터베이스로부터 삭제된 사용자의 수를 발행한다.
@Delete
fun deleteUsers(users: List<User>): Single<Integer>

커서로 직접 접근하기

@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
fun loadRawUsersOlderThan(minAge: Int): Cursor
  • 반환되는 행에 대해 직접적인 접근을 위한 반환되는 타입을 Cursor 객체로 만들 수 있다.
  • 행의 존재 여부나 행에 포함된 값을 보장하지 않으므로 커서 API를 사용하는 것을 권장하진 않는다.

여러 테이블 쿼리하기

@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>
  • 일부 쿼리는 여러 테이블에 접근해 조인을 해야할 수도 있다.
  • 또한 응답이 Flowable 또는 LiveData 처럼 Observable 타입인 경우 Room은 쿼리에서 참조된 모든 테이블을 주시한다.
  • 위의 예제는 테이블을 조인해 책을 빌리는 사용자를 포함하는 테이블과 현재 대출 중인 책에 대한 데이터를 포함하는 테이블 간 정보를 통합하는 방법을 보여준다.
@Query(
    "SELECT user.name AS userName, book.name AS bookName " +
    "FROM user, book " +
    "WHERE user.id = book.user_id"
)
fun loadUserAndBookNames(): LiveData<List<UserBook>>

data class UserBook(val userName: String?, val bookName: String?)
  • 쿼리에서 data 클래스를 반환할 수도 있다.

코루틴과 비동기 메서드 작성하기

@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>
  • 코틀린의 코루틴 기능을 사용하면 suspend 키워드를 DAO 메서드에 추가해 비동기식으로 만들 수 있다.
  • 이를 통해 메인 스레드에서 실행할 수는 없다.

트랜잭션 메서드 만들기

@Dao
abstract class SongDao {
    @Insert
    abstract fun insert(song: Song)
    @Delete
    abstract fun delete(song: Song)
    @Transaction
    fun insertAndDelete(newSong: Song, oldSong: Song) {
        // 이 메서드 내부 코드는 단일 트랜잭션으로 동작한다.
        insert(newSong)
        delete(oldSong)
    }
}
  • 추상 Dao 클래스에서 비추상 메서드를 구현할 때 @Transaction 어노테이션을 추가할 수 있다. 이를 트랜잭션 메서드라 하며, 이 메서드 내부에서는 Dao 클래스 내의 메서드를 호출할 수 있다.

관계있는 엔티티 가져오기

@Entity
data class Pet (
    @PrimaryKey
    var id = 0,
    var userId = 0,
    var name: String = null
)

class UserNameAndAllPets {
    var id = 0
    var name: String = null

    @Relation(parentColumn = "id", entityColumn = "userId")
    var pets: List<Pet> = null
}

@Dao
interface UserPetDao {
    @Query("SELECT id, name FROM User")
    fun loadUserAndPets(): List<UserNameAndAllPets>
}
  • @Relation은 data class에서 관계 엔티티를 자동으로 가져오는데 사용할 수 있는 편리한 어노테이션이다.
  • Data class가 쿼리에서 리턴되면, 해당 data class 안 모든 관계도 Room에 의해 가져온다.
  • @Relation 어노테이션이 달린 필드의 유형은 반드시 List 또는 Set이어야 한다.
data class User (
    var id = 0
    ...
)

data class PetNameAndId (
    var id = 0,
    var name = ""
)

class UserAllPets {
    @Embedded
    var user: User = null

    @Relation(parentColumn = "id",
            entityColumn = "userId",
            entity = Pet::class)
    var pets: List<PetNameAndId> = null
}

@Dao
interface UserPetDao {
    @Query("SELECT * FROM User")
    fun loadUserAndPets(): List<UserAllPets>
}
  • 엔티티 타입은 리턴 타입에서 유추되지만, 다른 객체를 반환하려면 어노테이션에 entity 속성을 지정할 수 있다.
  • 앞의 예제에서 PetNameAndId는 일반 data class이지만 모든 필드는 @Relation 어노테이션 entity 속성에 정의된 엔티티(Pet)에서 가져온다.
  • PetNameAndId도 관계를 정의할 수 있으며 모든 관계에 대해서도 자동으로 가져온다.
class UserAllPets {
    @Embedded
    var user: User = null

    @Relation(parentColumn = "id",
            entityColumn = "userId",
            entity = Pet::class,
            projection = ["name"])
    var pets: List<PetNameAndId> = null
}
  • 하위 엔티티에서 가져올 컬럼을 지정하려는 경우 @Relation 어노테이션의 projection 속성을 사용할 수 있다.
  • @Relation 어노테이션은 data class에서만 사용할 수 있으며, 엔티티 클래스는 관계를 가질 수 없다.
  • @Relation 어노테이션이 붙은 필드는 생성자 매개 변수일 수 없고, 접근 제한자가 public이거나 public setter 메서드가 있어야 한다.

✏️ Database 생성

  • RoomDatabase를 상속 받은 UserDatabase를 생성한다.
import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [User::class], version = 1)
abstract class UserDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}
  • @Database 어노테이션은 데이터베이스와 연결되는 entity와 데이터베이스 항목이 변하는 경우 바꿔줘야 하는 버전을 작성한다.
  • 이 때 데이터베이스는 추상 클래스로 작성해야 하며 반환값이 DAO인 매개 변수가 없는 추상 메서드를 포함해야 한다.

✏️ 코드에서 사용하기

1) DB 객체 생성

val database = Room.databaseBuilder(
        applicationContext, 
        UserDatabase::class.java, 
        "minha_db"
    ).fallbackToDestructiveMigration()
    .allowMainThreadQueries()
    .build()

// 인터페이스 객체 할당
mUserDao = database.userDao()
  • Room.databaseBuilder의 매개변수
    • Context : 보통 applicationContext
    • Class : @Database로 어노테이션된 추상 클래스
    • name : 데이터베이스 파일 이름(개발자 지정)
  • fallbackToDestructiveMigration() : 스키마(Database) 버전 변경 가능
  • allowMainThreadQueries() : Main Thread에서 DB에 입출력을 가능하게 함

2) DB 사용

// insert
val user1 = User()
user2.firstName = "minha"
user2.lastName = "Lee"
user1.age = "25"
mUserDao.setInsertUser(user1)

// update
val user2 = User()
user2.id = 1
user2.firstName = "gildong"
user2.lastName = "Hong"
user2.age = "33"
mUserDao.setUpdateUser(user2)

// delete
val user3 = User()
user3.id = 2
mUserDao.setDeleteUser(user3)

// select
val userList = mUserDao.getUserAll()

for(user in userList) {
    binding.tvResult.append("name : ${user.firstName}, age : ${user.age}\n")
}
profile
🤗🤗🤗

0개의 댓글