✏️ 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 사용
val user1 = User()
user2.firstName = "minha"
user2.lastName = "Lee"
user1.age = "25"
mUserDao.setInsertUser(user1)
val user2 = User()
user2.id = 1
user2.firstName = "gildong"
user2.lastName = "Hong"
user2.age = "33"
mUserDao.setUpdateUser(user2)
val user3 = User()
user3.id = 2
mUserDao.setDeleteUser(user3)
val userList = mUserDao.getUserAll()
for(user in userList) {
binding.tvResult.append("name : ${user.firstName}, age : ${user.age}\n")
}