Kotlin + Android でRealmをつかってみた。


この記事は、OthloTech Advent Calendar 2017 7日目の記事として書かれています。

今日、明日と自分が担当させていただきます。飽きないように記事の内容はガラッと変えていこうかと思うのでよろしくお願いします。

今日はモバイル用軽量データベースであるRealmについてです。

対象読者

  • Androidでデータベースを使いたい人。
  • SQLiteからRealmに移行したい人。
  • KotlinでのAndroidアプリ開発に興味がある人。

記事の内容

  • Kotlin + Androidアプリ制作でRealmをアクティベートするためにやらなければならないこと(必要最低限)をまとめていきます。
  • AndroidもKotlinも初心者である自分がRealmをアクティベートするまででハマったことをまとめていきます。

開発環境

  • Android Studio 3.0.1
  • Kotlin 1.2.0
  • Realm 4.2.0

Realmってなに?

上の方にも書いておいたのですがモバイル向けの軽量データベースです。
SQLiteの代替になるといわれているデータベースでiOSやAndroidでは広く使われています。
動作はSQLiteよりも高速で、SQLではないので柔軟にデータベースを構成できます。
以下RealmとSQLiteの性能比較です。Realmの方が圧倒的に高いパフォーマンスを発揮していることが見て取れますね。

以下ページを参照しました。
https://tech.iheart.com/performance-comparison-of-realm-and-sqlite-on-ios-6df1d51e6a07

細かい部分は公式を見るといい面白いと思います。
日本語のドキュメント古いんであてになりません。

Realm公式サイト
https://realm.io/

Gradleの設定

まずはRealmのビルド環境を整えます。
build.gradle(Project)にはこのように記述します。記述箇所は大きく変更していなければ上の方にあるかなと思います。
バージョンに関しては最新(2017/12/07時点)である4.2.0を利用しています。

build.gradle(project)
buildscript {
    // 省略
    }
    dependencies {
        // 省略
        classpath "io.realm:realm-gradle-plugin:4.2.0"
    }
}

次にbuild.gradle(App)に関する追記です。こちらも変更してなければ上の方にあるかなと思います。applyとかやたら書いてあるところですね。

build.gradle(app)
apply plugin: "realm-android"

ここまででビルド環境は整いました。

Realmの初期設定

いろいろやり方はありますがとりあえず何も考えなくていい初期化をします。
今回は本当に最低限ということを目指していくのでMainActivityでこねくり回してますが適宜クラスに抽出とかはした方がいいと思います。
後ほど触れますが、ここでRealmの設定をいじることができます。Migrationの設定は少なくともいじらないと面倒くさいので。

MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    // 省略

    Realm.init(this)
    mRealm = Realm.getDefaultInstance()
}

ついでにRealmの削除について定義します。
自然なのでonDestroyを利用してそこでcloseしています。

MainActivity.kt
override fun onDestroy() {
    super.onDestroy()
    mRealm.close()
}

Modelの作成

Javaじゃないのでゲッタとセッタは不必要です。やったね。
Kotlinっぽく書くってことでコンストラクタで変数の処理をしています。これでもプロパティへのアクセスはできるようになります。(デフォルト値設定する必要あり)
Realmで作るときの注意点として、RealmObjectを継承する必要があります。
アノテーションに関していうとこんなところでは書ききれないので公式ドキュメントを見てほしいなと思います。
ちなみにPrimaryKeyを設定しないと値の変更ができなくなります。

Book.kt
open class Book(
        @PrimaryKey open var id : String = UUID.randomUUID().toString(),
        @Required open var name : String = "",
        open var price : Long = 0
) : RealmObject() {}

ハマったこと

ここに記述したクラスや変数にはopenという修飾子がついています。
Realmのデータベースはテーブルにアクセスするために中でModelクラスを継承する必要があります。
しかしながらKotlinのデフォルトではJavaでいうfinalクラスで定義され、継承ができなくなってしまいます。
この状況を防ぐためにopen修飾子をつけています。

実際に操作してみる

CRUDを実際に行っています。本当は文字で表示したりとかした方が面白いと思いますが面倒くさいのでLogに表示だけ。BreakPointはって中身見れば動作していることがわかりやすいとおもいます。
コードはとっても適当に書いています。もっといい方法もあると思います。
そもそもonCreateで処理する意味ないです。

MainActivity.kt
    override fun onCreate(savedInstanceState: Bundle?) {
        // 省略

        // create test
        create("test1",1)
        create("test2")

        // read test
        val getData = read()
        getData.forEach {
            Log.d("debug","name :" + it.name + "price : " + it.price.toString())
        }

        // update test
        update(getData.first()!!.id, "updated")

        val getUpdatedData = read()
        getUpdatedData.forEach {
            Log.d("debug","name :" + it.name + "price : " + it.price.toString())
        }

        // delete test
        delete(getData.first()!!.id)

        val getDeletedData = read()
        getDeletedData.forEach {
            Log.d("debug","name :" + it.name + "price : " + it.price.toString())
        }

    }

データの追加について

主キーがあるのでオブジェクトを作るときにはクラスに加えて主キーを引数にしています。
ここでのcreateでは主キーのみを持つインスタンスの生成→DBの更新というような形で行っています。
まぁ同一トランザクションなので変わらないですが…
ほかにもいろいろやり方はありますが紹介しきれないので適宜調べていただければと思います!

MainActivity.kt
    fun create(name:String, price:Long = 0){
        mRealm.executeTransaction {
            var book = mRealm.createObject(Book::class.java , UUID.randomUUID().toString())
            book.name = name
            book.price = price
            mRealm.copyToRealm(book)
        }
    }

データの読み取りについて

普通に全データを読みだしています。
equalToを利用すれば絞り込みができるし、昇順降順などの並べ替えも行えます。
変更をしないのでトランザクションは張っていません。

MainActivity.kt
    fun read() : RealmResults<Book> {
        return mRealm.where(Book::class.java).findAll()
    }

データの更新について

データを取得して、その値を変更しています。
メソッドのコード的には該当idの要素があるかはわからないので非ヌル修飾子をつけています。
実際にコードを書く時には絶対にエラー処理をしてください。僕と約束です。

MainActivity.kt
    fun update(id:String, name:String, price:Long = 0){
        mRealm.executeTransaction {
            var book = mRealm.where(Book::class.java).equalTo("id",id).findFirst()
            book!!.name = name
            if(price != 0.toLong()) {
                book.price = price
            }
        }
    }

データの削除について

これ、いい書き方知らないんですよね。とりあえずレコードの配列みたいなものを受け取ってその0番目を削除するように書いています。
先頭を削除とか末尾を削除とかの関数も用意されています。

MainActivity.kt
    fun delete(id:String){
        mRealm.executeTransaction {
            var book = mRealm.where(Book::class.java).equalTo("id",id).findAll()
            book.deleteFromRealm(0)
        }
    }

ハマったこと

デバッグで変数の中身を細かく見ようとすると、正確に値が表示されませんでした。
こんな感じでnullとか0とか列挙されるんですね。おいおい値正確に入ってねーじゃねーかって感じになります。

しかし、この状態では見ることができるし、代入してもうまいこと値が入っています。
ちなみに自分はこれで1時間溶かしました。

Migrationの設定

今の状態ではModelクラスを変更した場合にアプリが落ちるという状態になってしまっています。
理由としては変更をした場合にスキーマが変更されたときの処理が記述されていないためというものになります。
つまりRealmではスキーマの変更をしたときにどのように変更をするかを適切に記述する必要があります。そうすることでアプリ実行時に変更があるのかを判断し、スキーマの変更を行います。
しかし、これは少し大変である + 場合により必要なことが大きく変わるため、ここではModel変更後の実行時に落ちないようにします。下記コードでは変更がかかった場合にDBを削除し再構成を行います。

MainActivity.kt
Realm.init(this)
val realmConfig = RealmConfiguration.Builder()
        .deleteRealmIfMigrationNeeded()
        .build()
mRealm = Realm.getInstance(realmConfig)

おわりに

いかがだったでしょうか?
KotlinもRealmもAndroid Studio 3.0もまだまだアップデートが頻繁にあるのでいつまで使えるかはわかりませんが、本記事を読んでRealmのアクティベートで時間を使う人が1人でも減れば幸いです。
ソースコードはGitHubにアップしておきますので適宜ご利用ください。
それでは皆さん快適なハックライフを!