ORM 래퍼 라이브러리 Room을 Realm으로 바꾸기 (Kotlin)

잘 도메인 구동 설계에서는 「데이터베이스의 구현은 신경쓰지 말라」라고 말합니다만, 실제로 DB주위를 신경쓰지 않고 개발하는 것은 적지요. DB의 구현을 바꿀 수 있다고 해도 실제로 전환하는 사람도 드물지 않습니다. 장점은 전혀 없지만 시험에 해 보려고 생각합니다. 구체적으로 Realm으로 작성한 DB 액세스를 Room으로 바꿉니다.

Kotlin용 Room 샘플 다운로드



정확하게는 Room & Rx Java (Kotlin)이지만 AndroidStudio에서 샘플을 다운로드할 수 있습니다.
프로젝트를 열지 않은 화면 또는 File>new>Import Sample에서


다만 프로젝트에 문제가 있어 빌드할 수 없고, RxJava는 사용하지 않기 때문에 Persistance 디렉토리의 내용만 받습니다.
entity
@Entity(tableName = "users")
data class User(@PrimaryKey
                @ColumnInfo(name = "userid")
                val id: String = UUID.randomUUID().toString(),
                @ColumnInfo(name = "username")
                val userName: String)

공식적으로 data class가 서포트되고 있는 것은 기쁘네요.

다오
@Dao
interface UserDao {

    /**
     * Get a user by id.

     * @return the user from the table with a specific id.
     */
    @Query("SELECT * FROM Users WHERE userid = :id")
    fun getUserById(id: String): Flowable<User>

    /**
     * Insert a user in the database. If the user already exists, replace it.

     * @param user the user to be inserted.
     */
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUser(user: User)

    /**
     * Delete all users.
     */
    @Query("DELETE FROM Users")
    fun deleteAllUsers()
}

Flowable은 RxJava 클래스이므로 나중에 제거합니다. 이것에 한정되지 않고 List등의 컨테이너도 사용할 수 있습니다.

database
@Database(entities = arrayOf(User::class), version = 1)
abstract class UsersDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object {

        @Volatile private var INSTANCE: UsersDatabase? = null

        fun getInstance(context: Context): UsersDatabase =
                INSTANCE ?: synchronized(this) {
                    INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
                }

        private fun buildDatabase(context: Context) =
                Room.databaseBuilder(context.applicationContext,
                        UsersDatabase::class.java, "Sample.db")
                        .build()
    }
}

컴패니언 오브젝트를 사용하고 있습니다만 Kotlin이므로 Singleton의 오브젝트를 따로 만드는 편이 그것같은 생각이 듭니다.

모듈 만들기



DDD이므로 Database는 모듈에 격리됩니다.
기본 프로젝트는 이전에 소개한 입니다.


build.gradle는 이런 느낌.
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"

    // Room用に追加
    implementation "android.arch.persistence.room:runtime:1.0.0"
    kapt "android.arch.persistence.room:compiler:1.0.0"

    // DDDなのでモデルのモジュールに依存する
    compile project(path: ':fehsbattlemodel')
}

entity를 Realm용으로 만든 클래스에서 복사합니다.
@RealmClass
open class RealmArmedHero(
        @PrimaryKey
        var nickname: String = "",
        var baseName: String = "",
        var weapon: String = "NONE",
        ...
) : RealmObject() {
    fun toModelObject(): ArmedHero {}
}
@Entity(tableName = "heroes")
data class RoomArmedHero(
        @PrimaryKey
        var nickname: String = "",
        var baseName: String = "",
        var weapon: String = "NONE",
        ...
) {
    fun toModelObject(): ArmedHero {}
}

Realm은 Entity가 되는 클래스를 상속해 기능을 자동 생성하는 편리상, @RealmClass 과 open, RealmObject 의 상속이 필요합니다.
한편, Room측은 Entity의 클래스는 그대로 사용합니다. @Entity (tableName = "") 어노테이션을 붙이기만 하면 됩니다. 굳이 컬럼명은 지정하지 않고 최대한 손을 뽑아 봅니다.

dao는 Realm 측에도 대응하는 것이 있지만 이름을 잘 모르겠습니다. 샘플은 Content라는 이름이었습니다만….
object RealmArmedHeroContent : RealmContent<ArmedHero>() {
    /** realmのkotlin用ハンドラ */
    private var realm: Realm by Delegates.notNull()

    /** 初期化ブロック。テーブル変更時などはここでマイグレーションすることになる */
    init {
        realm = Realm.getDefaultInstance()

        realm.executeTransaction {
            //            realm.deleteAll()
        }
    }

    override fun delete(item: ArmedHero): Int {
        val results = realm.where(RealmArmedHero::class.java).equalTo("nickname", item.name).findAll()
        realm.executeTransaction {
            results.deleteAllFromRealm()
        }
        return results.size
    }

    override fun deleteById(id: String): Int {
        val results = realm.where(RealmArmedHero::class.java).equalTo("nickname", id).findAll()
        realm.executeTransaction {
            results.deleteAllFromRealm()
        }
        return results.size
    }

    override fun createOrUpdate(item: ArmedHero): ArmedHero {
        item.apply {
            realm.executeTransaction {
                realm.copyToRealmOrUpdate(RealmArmedHero(name, baseHero.name, weapon.value, refinedWeapon.value, assist.value, special.value, aSkill.value, bSkill.value, cSkill.value, seal.value, rarity, levelBoost, boon.name, bane.name
                        , defensiveTerrain, atkBuff, spdBuff, defBuff, resBuff, atkSpur, spdSpur, defSpur, resSpur))
            }
        }
        return item
    }

    override fun allItems(): List<ArmedHero> {
        return heroDao.allHeroes().map { e -> e.toModelObject() }
    }

    override fun getById(id: String): ArmedHero? = heroDao.getHeroById(id)?.toModelObject()

}

※ 클래스를 직접 지정하는 조금 오래된 코드입니다.
@Dao
interface HeroDao {

    @Query("SELECT * FROM heroes WHERE nickname = :id")
    fun getHeroById(id: String): RoomArmedHero

    @Query("SELECT * FROM heroes")
    fun allHeroes(): List<RoomArmedHero>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertHero(hero: RoomArmedHero)

    @Query("DELETE FROM heroes")
    fun deleteAllHeroes()

    @Query("DELETE FROM heroes WHERE nickname = :id")
    fun deleteHero(id: String)
}

@Query 어노테이션 중에 SQL을 작성합니다. @Insert (onConflict = OnConflictStrategy.REPLACE)에서 create/update가 될 것 같습니다. (시도 잊어)
Room 측의 Dao는 실제로 리포지토리를 상속하고 Dao를 호출하는 객체가 필요합니다.
object RoomArmedHeroContent : ModelObjectRepository<ArmedHero> {

    var appContext: Context? = null
    val heroDao get() = UsersDatabase.getInstance(appContext!!).heroDao()

    override fun delete(item: ArmedHero): Int {
        heroDao.deleteHero(item.name)
        return 1
    }

    override fun deleteById(id: String): Int {
        heroDao.deleteHero(id)
        return 1
    }

    override fun createOrUpdate(item: ArmedHero): ArmedHero {
        item.apply {
            heroDao.insertHero(RoomArmedHero(name, baseHero.name, weapon.value, refinedWeapon.value, assist.value, special.value, aSkill.value, bSkill.value, cSkill.value, seal.value, rarity, levelBoost, boon.name, bane.name
                    , defensiveTerrain, atkBuff, spdBuff, defBuff, resBuff, atkSpur, spdSpur, defSpur, resSpur))
        }
        return item
    }

    override fun allItems(): List<ArmedHero> {
        return heroDao.allHeroes().map { e -> e.toModelObject() }
    }

    override fun getById(id: String): ArmedHero? = heroDao.getHeroById(id)?.toModelObject()
}

하는 일은 거의 같습니다. SQL 상당의 것을 리포지토리에 쓸지 Dao에 쓸지 어떨지 정도입니다.

Entity가 늘어난 DB는 이런 느낌으로.
@Database(entities = arrayOf(User::class, RoomArmedHero::class), version = 1)
abstract class UsersDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    abstract fun heroDao(): HeroDao

    companion object {

        @Volatile private var INSTANCE: UsersDatabase? = null

        fun getInstance(context: Context): UsersDatabase =
                INSTANCE ?: synchronized(this) {
                    INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
                }

        private fun buildDatabase(context: Context) =
                Room.databaseBuilder(context.applicationContext,
                        UsersDatabase::class.java, "Sample.db").allowMainThreadQueries()
                        .build()
    }
}

@Database (entities = arrayOf(User::class, RoomArmedHero::class), version = 1)에 대상 Entity를 추가하여 Dao도 늘립니다.
allowMainThreadQueries()는 다른 스레드로 나누지 않고 액세스하기 위한 기술입니다. 번거롭기 때문에 추가했습니다만 없어서 끝낼 수 없는 편이 좋을 것입니다.

테스트


@RunWith(AndroidJUnit4::class)
class RoomInstrumentedTest {
    @Test
    @Throws(Exception::class)
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getTargetContext()

        appContext.deleteDatabase("Sample.db")

        RoomArmedHeroContent.appContext = appContext
        val modelHero = ArmedHero(StandardBaseHero.get("エフラム")!!,"new エフラム")
        RoomArmedHeroContent.createOrUpdate(modelHero)
        val insertedArmedHero = RoomArmedHeroContent.getById("new エフラム")
        assertEquals("new エフラム",insertedArmedHero!!.name)
    }
}

Model에 선언한 인터페이스를 경유해 보통에 액세스 할 수 있었습니다. 실기에서도 똑같이 바꾸어 움직일 수 있습니다. 동시에 사용할 수도 있었지만 반드시 의미는 없을 것입니다.

감상



· 양쪽 안드로이드에 의존하고 있기 때문에 이번에는 의미는 없지만, 서버 사이드에 이식할 때에 어노테이션에 SQL을 쓰는 라이브러리, 예를 들어 myBatis와 공유한다면 꽤 의미가 있을지도?
・Realm 쪽이 여러가지 편이지만 SQL의 자동 생성 등이 얽혀 오면 Room도 나쁘지 않나?
・Realm은 컬럼이 늘어나는 만큼 마이그레이션 빼도 움직이기도 하지만 Room은 엄격하다

좋은 웹페이지 즐겨찾기