📍 developers
https://developer.android.com/training/data-storage/room
https://developer.android.com/training/data-storage/room/defining-data#search
이 글은 개인적인 공부를 위해 developers의 글을 직접읽고 번역하며 작성한 글입니다. 번역의 오류가 있을 가능성이 있습니다. 개인적인 첨언이 있을수있습니다
Room 을 이용하여 Local DataBase에 data 저장하기
사소하지 않은 구조화된 데이터를 처리하는 앱은 해당 데이터를 로컬에 유지하면서 많은 이점을 얻을 수 있다. 가장 일반적인 사용 사례로는 데이터 조각을 캐시하는것이다. 그래서 네트워크에 연결 불가능한 경우에도 여전히 사용자가 컨텐츠를 검색할수 있도록 하는것
Room은 지속적인 라이브러리 이다 . SQLite 위에서 SQLite의 모든 기능을 활용하면서 유연하게 데이터 베이스에 접근하도록 도와주는 추상적인 layer를 제공한다.
- 컴파일 타임에 SQL 쿼리의 유효성 검사
- 반복과 에러가 발생하기쉬운 boilerplate code를 최소화한 편리한 annotations
- migration이 쉬움
위와 같은 이유로 SQLite API를 직접접근 하는것 대신 Room을 이용하는것을 적극 추찬한다
Setup
build.gradle
dependencies {
def room_version = "2.4.1"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// optional - RxJava2 support for Room
implementation "androidx.room:room-rxjava2:$room_version"
// optional - RxJava3 support for Room
implementation "androidx.room:room-rxjava3:$room_version"
// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
// optional - Paging 3 Integration
implementation "androidx.room:room-paging:2.4.1"
}
주요 컴포넌트
Room에는 세개의 주요 컴포넌트들이 있다
- Database Class - 데이터 베이스를 보유하고 연결을 위한 기본 엑세스 지점 역할을 하는 클래스
- Data entities - db table을 나타냄
- DAO(Data Access Object) - db에 query 사용할 수 있도록 method를 제공
Data Base class는 DB와 연결된 DAO인스턴스를 앱에 제공 한다
이후 앱은 DAO를 사용하여, 관련된 entity object의 인스턴스로, data를 검색할 수 있다
또한 앱은 정의된 data entities를 이용하여 상응하는 테이블의 row를 update 및 insert 할 수 있다 .
→ 해당 그림은 위에서 설명한바를 나타내는듯 하다 data base 에서 DAO 인스턴스를 받는다.
→ Data Access Object는 db에서 entities를 얻고, back에서 지속적으로 db를 change할수있다
→ entities 에서 data get / set 할수있다
→ 정도로 해석해봤다..
Single DAO, Single entity 를 이용한 Room 예제
DataEntity
아래의 코드는 User data entity를 정의한다. 각 User class의 instance는 user 테이블의 row를 나타낸다
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
- Defining data using Room entities
- @Entity
- Entity annotation을 통해 room entity를 class로 정의 할 수 있다.
- entity는 db에 상응하는 테이블에 각 column과 한개 이상의 primary key를 포함한다.
- data class property의 접근자가 public이거나 getter와 setter를 포함해야지 Room에서 data를 access할수 있따.
- 기본적으로 Room은 class 이름을 table이름으로 사용하는데, 만약에 다른이름을 원하면 @Entity annotation에 tableName property로 지정 가능하다.
- 또 field name을 column 이름으로 사용하는것이 기본적인데 다른이름을 원하면 @ColumnInfo annotation의 name property를 사용하여 설정할 수 있다.
-
@Entity data class User( @PrimaryKey val id: Int, val firstName: String?, val lastName: String? ) // 기본적으로 User table이름의 firstName.. column @Entity data class User( @PrimaryKey val uid: Int, @ColumnInfo(name = "first_name") val firstName: String?, @ColumnInfo(name = "last_name") val lastName: String? ) // 이름 직접지정
- primary key 정의하기
- 각 room 의 entity는 반드시 pk가 정의되어 있어야 한다 - 테이블에서 각행을 유니크하게 식별가능하도록
- 가장 확실한 방법은 single column에 @PrimarKey 값을 주는것이다.
- @PrimaryKey의 property중 autoGenerate 를 true로 주면 id값을 자동으로 부여한다
- composite pk 정의하는법
- 유니크한 식별을 위해 multiple columns의 조합이 필요한경우 composite primary key를 지정할수 있다
- @Entity annotation의 primaryKeys property 를 사용하여 key를 list하여 정의할수있다
@Entity(primaryKeys = ["firstName", "lastName"] data class User( val firstName: String?, val lastName: String?, )
- Ignore fields
- 기본적으로 Room은 entity안에있는 field들을 모두 column으로 생성한다
- 만약 어떤 필드가 지속하고싶지 않으면 @Ignore annotation을 이용하면 된다
- ex) @Ignore val picture: Bitmap?
- 상속을 통해 필드가 상속되는경우 Entity annotation의 ignoredColumns property를 사용하는것이 더 쉽다.
open class User { var picture: Bitmap? = null } @Entity(ignoredColumns = ["picture"]) data class RemoteUser( @PrimaryKey val id: Int, val hasVpn: Boolean ) : User()
- 테이블 search 지원
- room은 db테이블을 검색을 쉽게 만드는 여러가지 타입의 annotation을 지원한다
- minSdkVersion 16 이상에서 full-text 검색을 지원
- full-text search
- 만약 앱이 full-text search(FTS)를 통해 매우 빠른 db접근이 필요하면 FTS3, FTS4 SQLite확장모듈을 이용하여 virtual 테이블을 사용하라
- FTS는 SQLite에서 제공하는 전문검색 vritual table로 긴 데이터 가령 email 전문 같은 데이터에서 like 같은 연산을 사용하는 경우 빠른 속도록 보장하도록 디자인되어있다고 한다
- 암튼 사용하려면 @Fts4와 같은 annotation 추가해야하고, 반드시 pk로 “rowid”라는 이름의 integer type값을 지정해야한다
// 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? )
- 특정 columns index지정
- 만약 FTS가 지원되지 않는 SDK 버전이라면 쿼리속도를 높이기 위해 특정 columns에 index를 지정할 수 있다.
- indices property를 @Entity annotation에 list형태로 단일 혹은 복합적인 인덱스를 나열하헤라
@Entity(indices = [Index(value = ["last_name", "address"])])
- 특정 필드, 또는 필드 그룹이 unique 해야하는 경우 @Index annotation의 unique property를 true로 설정해서 고유함을 명시할 수있다. 아래 예제는 pk는 아니지만 unique함이 보장되는 columns 조합을 명시함
@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? )
- @Entity
Data Access Object(DAO)
- 아래 DAO class는 user table에 접근하는 함수를 제공하는 클래스이다
@Dao interface UserDao { @Query("SELECT * FROM user") fun getAll(): List<User> @Query("SELECT * FROM user WHERE uid IN (:userIds)") fun loadAllByIds(userIds: IntArray): List<User> @Query("SELECT * FROM user WHERE first_name LIKE :first AND " + "last_name LIKE :last LIMIT 1") fun findByName(first: String, last: String): User @Insert fun insertAll(vararg users: User) @Delete fun delete(user: User) }
- Accessing data using Room DAOs
- Room 라이브러리 사용하여 앱에 데이터를 저장하려고 할때, Data Access Object와 interaction해야한다
- 각 DAO는 DB 접근을 위한 추상합수를 제공한다.
- compile 시점에 Room은 자동으로 우리가 정의한 DAO 구현체를 생성한다
- 직접 쿼리하는법이나 query builder를 이용하는 대신 DAO를 이용하여 DB에 접근함으로 아키택처의 주요 법친인 관심사 분리를 유지할 수 있다.
- 그리고 mock db를 이용하여 앱을 test하기 쉬워진다
- 각 DAO는 interface나 abstract class 로 정의할 수 있다 . 기본적으로 보통 interface를 쓴다
- @DAO annotation을 써야한다
- @DAO interface는 property를 가질수 없고 여러 interacting을 위한 함수를 정의할 수 있다.
- 기본적인 insert, delete, select 함수를 정의한 예제
@Dao interface UserDao { @Insert fun insertAll(vararg users: User) @Delete fun delete(user: User) @Query("SELECT * FROM user") fun getAll(): List<User> }
- DAO 함수의 2가지 종류
- SQL 코드 없이 insert, delete, update,가능한 함수
- query 함수, 직접 쿼리 작성하는
- 편리한 함수들
- Insert
@Insert annotation은 관련된 테이블에 파라미터들을 insert해주는 함수를 정의할수 있도록 해줌→ insert 함수의 각 파라미터들은 반드시 @Entity annotation을 이용하여 생성한 entity class의 instance거나, entitiy class를 담은 collection 형태여야한다
→ 만약 insert함수에 하나의 파라미터만 선언하면 return type을 생성된 row의 rowID에 해당하는 값을 long1으로 받을 수 있다
→ 만약 array나 collection으로 parameter를 지정하면 long 대신 array 나 collection을 return한다 각 아이템에 대한 rowId가 담긴
→ insert 함수가 호출되면 Room은 상응하는 table에 entity instance를 insert한다 - interface UserDao { fun insertUsers(vararg users: User) fun insertBothUsers(user1: User, user2: User) fun insertUsersAndFriends(user: User, friends: List<User>) }
- update
@Insert와 흡사하다
@Update annotation은 특정 row를 update 하도록 함수를 정의할수 있게 한다.
Room은 넘겨받은 entity instance와 db의 row를 대응시키기 위해 PK를 사용한다.성공적으로 update된 행의 수를 나타내는 int값을 선택적으로 return 받을 수 있다.@Dao interface UserDao { @Update fun updateUsers(vararg users: User) }
만약에 PK가 같은 행이 없으면 아무 변화를 만들지 않는다 - Delete
@Delete annotation 사용해서 특정행 지우도록 정의가능
파라미터로 entity instance 전달
PK 사용하여 entity instance와 row 대응시킴, pk 같은 행 없으면 변화 없음@Dao interface UserDao { @Delete fun deleteUsers(vararg users: User) }
성공적으로 delete된 행의 수를 선택적으로 return 받을 수 있다. - Query methods
@Query annotation은 SQL문 을 작성하도록 해주고 그것을 DAO method로 제공해준다.
insert, delete, update보다 복잡한 기능이 필요한경우 사용
Room은 compile time에 쿼리를 검증한다 . 즉 runtime에 오류가 발생하는것이 아니라 컴파일할때 에러를 볼 수 있다는것
- 테이블의 columns subset return
대부분 모든 속성이 아닌 부분 colums만 필요하다. 자원을 아끼고 실행을 간소화 시기키 위해 필요한 필드만 쿼리해라
Room는 결과로 반환된 subset에 매핑할 수 있는 간단한 object를 반환할 수 있다 .
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>
- query에 간단한 parameters 넘기기
대부분 DAO method는 파라미터를 사용해서 필터링 연산을 수행할 수 있다.
룸은 함수의 파라미터를 쿼리에 바인딩 할수 있도록 지원한다
@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>
- query에 collection parameters 넘기기
실행전까지 몇개의 파라미터일지 모르는 경우가 생길수 있다. Room은 파라미터가 collection일때 런타임에 자동으로 확장한다.
@Query("SELECT * FROM user WHERE region IN (:regions)") fun loadUsersFromRegions(regions: List<String>): List<User>
- 여러 테이블을 사용하는 쿼리
몇 쿼리는 여러 테이블을 접근하여 결과를 내야할 수 있다. Join절을 쿼리에 쓰면 하나 이상의 테이블을 연결 할 수 있다.
return subset을 위한 객체 정의 가능// 세개의 테이블을 연결 예제 @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>
interface UserBookDao { @Query( "SELECT user.name AS userName, book.name AS bookName " + "FROM user, book " + "WHERE user.id = book.user_id" ) fun loadUserAndBookNames(): LiveData<List<UserBook>> // You can also define this class in a separate file. data class UserBook(val userName: String?, val bookName: String?) }
- multimap 반환
Room 2.4 이상부터 여러 테이블로부터 columns 를 검색할때 따로 추가적인 data class를 정의할 필요 없이 multimap으로 함수 반환을 할 수 있다.
예를들어 User와 Book의 Pair를 담고있는 custom data class를 만들어 해당 instance 들의 list를 return 하는 대신, User랑 Book을 직접 mapping하여 return 하도록 함수를 작설할수 있다.
이렇게 함수가 multimap의 형태를 return할때 Group By 절을 사용 할 수 있어 고급 계산 및 필터링을 위한 SQL기능을 활용 할 수 있다.@Query( "SELECT * FROM user" + "JOIN book ON user.id = book.user_id" ) fun loadUserAndBookNames(): Map<User, List<Book>>
만약에 전체 object를 다 매핑 할 필요가 없는경우 @MapInfo 주석에서 keyColumn, valueColumn property를 설정해서 특정 열간의 매핑을 반환할 수 있다.@Query( "SELECT * FROM user" + "JOIN book ON user.id = book.user_id" + "GROUP BY user.name WHERE COUNT(book.id) >= 3" ) fun loadUserAndBookNames(): Map<User, List<Book>>
@MapInfo(keyColumn = "userName", valueColumn = "bookName") @Query( "SELECT user.name AS username, book.name AS bookname FROM user" + "JOIN book ON user.id = book.user_id" ) fun loadUserAndBookNames(): Map<String, List<String>>
- 특별한 return types
- Paging 라이브러리를 이용하여 page를 나눈 쿼리
Room은 pageing library와 통합하여 pageinated queries를 지원한다. Room 2.3.0-alpha01 이상부터 DAO는 PagingSource 객체를 return 할 수 있다.
@Dao interface UserDao { @Query("SELECT * FROM users WHERE label LIKE :query") fun pagingSource(query: String): PagingSource<Int, User> }
- 직접 cursor 접근하기 (권장 X)
만약 개발할 앱이 로직상 반환될 rows 에 직접접근이 필요한 경우 DAO method의 return을 Cursor로 작성할 수 있다.
@Dao interface UserDao { @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5") fun loadRawUsersOlderThan(minAge: Int): Cursor }
- Paging 라이브러리를 이용하여 page를 나눈 쿼리
- 테이블의 columns subset return
- Insert
- Accessing data using Room DAOs
Database
- 아래 코드는 AppDatㄷbase 데이터베이스를 보유할 클래스를 정의하는 코드다. AppDatabase는 db의 구성을 정의하고, 앱의 main access point로의 역할을 한다. database class는 반드시 아래의 안정성 요구조건을 따라야한다.
- 반드시 @Database annotation을 사용해야하며, 해당 db와 관련된 모든 entites array를 해당annotation에 포함해야한다
- 반드시 RoomDatabase 추상클래스를 상속하여야한다
- Database와 연관된 각각의 DAO instance 를 반환하고 parameter가 없는 abstract method를 반드시 정의해야한다.
- 앱이 single process로 동작하면 AppDatabase 객체를 싱글톤 패턴으로 생성하라. RoomDatabase instance는 꽤 비용이 나간다. 그리고 single process에서는 multiple instances를 사용하는 경우가 드물다
- multiple process에서 실행되는 경우 빌더 호출에 enableMultiInstanceInvalidation()을 포함해서 효율적으로 사용해라
Usage
- 위에서 entity, DAO, database 다 만든담에 db instance를 생성해
- DataBase객체의 abstract method를 이용하여 DAO instance를 get한다.
- 받아온 DAO instance의 method를 활용하여 db와 상호작용하면된다.
// 1.
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
//2.
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
'Android' 카테고리의 다른 글
Firebase Cloud Messaging (FCM) - Android (0) | 2022.02.24 |
---|---|
Android 에서 MVC, MVP, MVVM 예제로 공부하기 (0) | 2022.02.17 |
Clean Architecture - Android에 적용하기 (0) | 2022.01.23 |
Gson vs kotlinx-serialization (0) | 2022.01.17 |
[Android] detekt 적용하기 (0) | 2021.08.01 |