Kotlin MultiPlatform Mobile에서 Realm Database를 사용하기 위해,
순수 Kotlin언어로 작성된 Realm-Kotlin을 소개합니다!
- sdk 설치
- Object Schema 정의
- Realm 열기
- Data 읽고 쓰기
- Reactive 형태로 Data 변화 감지
1. sdk 설치
- Kotlin MultiPlatform에서 설치하는 방법으로
해당 모듈의 build.gradle.kts 파일에서 realm, coroutines 관련 dependencies 및 plugins 추가- 내부적으로 Realm Kotlin은 coroutines를 사용하여 비동기 처리를 하기에 관련 라이브러리 필요함
- 주의사항 : Realm Kotlin 버전 1.3.0 이상 사용 시 프로젝트의 Kotlin 버전 1.7.20 이상 사용해야 함
(Coroutine 라이브러리는 1.6.0 이상 사용)
plugins {
id("io.realm.kotlin") version "1.5.0"
}
...
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("io.realm.kotlin:library-base:1.5.0")
}
}
}
}
2. Object Schema 정의
- Realm Database에서 사용할 data model 정의 시 RealmObject 상속
- 기본적인 데이터 타입뿐만 아니라 Primary Keys와 관계 형식의 데이터도 정의할 수 있음
- 관계형 프로퍼티
- To-One Relationship : 하나 이상의 다른 객체와 연결되지 않음을 의미하므로 타입 선언 시 반드시 nullable이어야 함
- To-Many Relationship : 리스트 형태의 RealmObject들을 정의하는 방법으로 RealmList<T>타입 선언
class GitUserRealm : RealmObject {
@PrimaryKey
var id: Int = 0
var login: String = ""
var name: String = ""
var company: String = ""
var follower: GitFollowerRealm? = null
var followers: RealmList<GitFollowerRealm> = realmListOf()
}
class GitFollowerRealm : EmbeddedRealmObject {
var id: Int = 0
var login: String = ""
var url: String = ""
}
- EmbeddedRealmObject 특징
(아직 공식 문서에 작성된 기능은 아니지만 realm-kotlin(Github Page)의 Pull Request 내역에서 찾음 - 2023/01/30 기준)- 단독으로 사용할 수 없고, 반드시 하나의 parent가 연결되어야 함
- @PrimaryKey 필드 사용 불가
- parent object 삭제 시 embedded 된 모든 objects 함께 삭제됨
- (Realm-Java의 @RealmClass(embedded = true)과 같은 역할)
3. Realm 열기
private val realm: Realm by lazy {
val configuration = RealmConfiguration.Builder(
schema = setOf(GitUserRealm::class, GitFollowerRealm::class)
).apply {
deleteRealmIfMigrationNeeded()
}.build()
Realm.open(configuration)
}
- Realm 초기화 시 정의한 스키마와 configuration을 지정할 수 있음
- RealmConfiguration 옵션
- deleteRealmIfMigrationNeeded() : migration 없이 기존 realm 스키마 변경 시 기존 데이터 삭제 후 새로운 스키마 정의
- encryptionKey(encryptionKey: ByteArray) : 64 byte 암호화 키 사용하여 realm file 암호화 및 복호화
(암호화되지 않은 realms 보다 10% 느림) - inMemory() : realm file에 쓰지 않고 메모리에서만 데이터를 관리하는 옵션으로 realm 또는 앱 종료 시 데이터가 유지되지 않음
4. Data 읽고 쓰기
- Create, Update, Delete 시 Frozen Architecture에 의해 write transaction block을 사용하여 live objects를 사용해야 함
- realm에서 제공하는 write transaction 방식은 2가지로
- writeBlocking : 작업이 완료될 때까지 호출한 thread를 blocking 함
(UI thread 사용 시 ANR 발생 가능성 있음) - write : Coroutines 사용하여 비동기적으로 작업을 처리함
realm에서 제공하는 default thread(Dispatcher) 사용
(호출 시 별도로 thread 변경할 필요 없음
- writeBlocking : 작업이 완료될 때까지 호출한 thread를 blocking 함
- realm에서 제공하는 write transaction 방식은 2가지로
// sync
fun insertBlocking() {
realm.writeBlocking {
copyToRealm(gitUserData, UpdatePolicy.ALL)
}
}
// async
suspend fun insert() {
realm.write {
copyToRealm(gitUserData, UpdatePolicy.ALL)
}
}
// MutableRealm에 정의된 interface
public fun <T : RealmObject> copyToRealm(instance: T, updatePolicy: UpdatePolicy = UpdatePolicy.ERROR): T
// Realm에 정의된 interface
public fun <R> writeBlocking(block: MutableRealm.() -> R): R
public suspend fun <R> write(block: MutableRealm.() -> R): R
- Create / Update
- write transaction 내에서 작업해야 함
- copyToRealm()은 새로운 object를 realm에 넣거나 존재하는 object를 업데이트
- copyToRealm()의 updatePolicy는 ERROR로 설정되어 있어 primary key가 중복되면 Exception이 발생함
-> updatePolicy를 UpdatePolicy.ALL 변경하면 매칭되는 key의 object 값들을 업데이트할 수 있음
(Realm-Java의 insertOrUpdate()와 같은 기능)
// sync
fun get() =
realm.query<GitUserRealm>()
.find()
// async as flow
fun getAsFlow(id: Int) =
realm.query<GitUserRealm>("id == '$id'")
.asFlow()
// MutableRealm에 정의된 interface
override fun <T : BaseRealmObject> query(
clazz: KClass<T>,
query: String,
vararg args: Any?
): RealmQuery<T>
// RealmResults, ResultsChange에 정의된 interface
public fun find(): RealmResults<T>
public fun asFlow(): Flow<ResultsChange<T>>
- Read
- query()는 조회하고자 하는 RealmObject와 query 내용과 매칭되는 RealmQuery를 반환함
- 반환된 RealmQuery의 결과를 조회하는 방법은 2가지로
- find() : 동기적으로 동작하며 query 시 호출한 thread를 사용함
(UI thread 사용 시 ANR 발생 가능성 있음) - asFlow() : query 결과를 Flow로 반환하여 CoroutineScope에서 비동기적으로 작업할 수 있음
- find() : 동기적으로 동작하며 query 시 호출한 thread를 사용함
- 주의사항
- Realm.query()를 통해 조회한 데이터의 결과는 frozen objects로 업데이트 및 삭제 시 write transaction block 안에서
- MutableRealm.query()로 조회하여 live objects를 반환받아 사용하거나
- MutableRealm.findLatest()를 통해 live objects로 변환하여 사용해야 함
- Realm.query()를 통해 조회한 데이터의 결과는 frozen objects로 업데이트 및 삭제 시 write transaction block 안에서
// sync
fun deleteBlocking() {
realm.writeBlocking {
val gitUser = query<GitUserRealm>().find()
delete(gitUser)
}
}
// async
suspend fun delete() {
realm.write {
val gitUser = query<GitUserRealm>().find()
delete(gitUser)
}
}
// MutableRealm에 정의된 interface
public fun delete(deleteable: Deleteable)
- Delete
- write transaction 내에서 작업해야 함
- MutableRealm.query()를 통해 지우고자 하는 live objects를 조회하여 반환된 RealmQuery 또는 RealmResults로 삭제
5. Reactive 형태로 Data 변화 감지
scope.launch {
gitDao.getAsFlow().collectLatest { user ->
user.toString()
}
}
- realm.query() 시 flow로 형태로 반환하고
- coroutineScpoe 안에서 데이터 처리
관련 코드는 https://github.com/Yeechaan/kmm-git 에 업로드 해두었고,
shared 모듈의 commonMain을 참고하면 된다.
realm-kotlin
공식문서 : https://www.mongodb.com/docs/realm/sdk/kotlin/
Github Page : https://github.com/realm/realm-kotlin