Realm でローカルのデータベースに大量のデータを追加したい

10万件くらいのデータを Realm に持ってそのデータを使って図鑑とかが見れる iOS アプリを作っている。固定データをわざわざ API から取得するのは無駄なので、最初からアプリ内にデータを保持する設計になっている。
そのアプリに2万件くらいのデータを追加しないといけなくなった。

ここで考慮しないといけないのは以下の2つで、これが達成できれば問題ない。

  • データベースの更新中にユーザがアプリを kill してもデータベースを壊さない
  • ユーザが作成したデータもあるので、それを消す / 壊してはいけない

というわけで、何に悩んだかとどうやったかをつらつら書いていく。

背景

実は今までもローカルのデータの更新をしていて、その時はまるっと Realm ファイルの入れ替えをしていた。アプデ前のアプリで使っていたデータベースを削除して、新しいデータベースをコピーしていた。

しかし前回のアップデートでユーザが任意のデータを作れるようになった。

つまり、今まで使っていたデータベースを削除できなくなってしまった。
ユーザが作成したデータを消さずに新たなデータを追加する必要がある。

悩み

データの更新に時間がかかる

更新データは text でアプリ内に持っており、そのファイルを forloop でくるくる回して Realm に write する。

更新処理は iPhone7 で5秒くらいかかったので、 iPhone5s とかはもうちょっとかかりそう。
5秒もあったら絶対に途中でアプリを閉じるユーザが出てくるので、途中で閉じられても問題ない作りにする。

どうするか

1 の案で実装した。1以外はボツ案。

1. realm.write + UserDefaults でフラグ管理

Realm にデータを書き込む際に try realm.write をするが、これは全ての処理が完了した時に commit される。
そのため、更新処理中にアプリを kill されても commit はされず、また最初からやり直すことができる。

そして更新処理が完了したら UserDefaults のフラグを true にして今後は実行されないようにするだけ。これだけの簡単な仕組みでうまくできた。

コードにするとこんな感じ:

// https://github.com/yaslab/CSV.swift
// import CSV

let filename = "更新ファイル名"

if UserDefaults.standard.bool(forKey: filename) {
    return
}

guard let moveCSVPath = Bundle.main.path(forResource: filename, ofType: "csv"),
    let stream = InputStream(fileAtPath: moveCSVPath) else {
        return
}

try! realm.write {
    for row in try! CSV(stream: stream, hasHeaderRow: false) {
        let model = Moves()
        model.id = Int(row[0])!

        // ...

        realm.add(model)
    }
}

UserDefaults.standard.set(true, forKey: filename)
UserDefaults.standard.synchronize()

2. 何行目まで write したか UserDefaults に保持する

Realm にデータを書き込む際に try realm.write をするが、これは全ての処理が完了した時に commit される。

最初はこの仕様を知らなくて「途中まで write してアプリを kill されたら最初の方のデータが2重登録されちゃうやんけ...」などと考えていた。
なので、何行目まで write したのか保持してデータを重複させない仕組みを作らねば、などと考えて以下のように実装した。

let count = UserDefaults.standard.integer(forKey: filename)

try! realm.write {
    for (index, row) in try! CSV(stream: stream, hasHeaderRow: false).enumurated() {
        if index < count { continue }
        let model = Moves()
        model.id = Int(row[0])!

        // ...

        realm.add(model)

        UserDefaults.standard.set(index + 1, forKey: filename)
        UserDefaults.standard.synchronize()
    }
}

これだと、途中でアプリを kill したとしても前回どこまで write したかを覚えられるので write 処理が重複することがない。
しかし、 realm.write が完了する前にアプリを kill してもそれは commit されていないので、これだと逆に if index < count { continue } されたデータが保存されなくなる問題が生まれる、というか生まれた。

あと UserDefaults に書きまくっているのでパフォーマンスがやばくて30秒くらいかかった。

3. 今まで通りデータベース丸ごと入れ替える案

  • ユーザが登録したデータを一時的に text ファイルなどに避難させる
  • その隙に既存のデータベースを新しいデータベースをコピー
  • そして避難させたデータを新しいデータベースに write

でいけそうかもと思ったんだけど、なんか不整合起こりそうな気がしてやめた。

4. UserDefaults でフラグ管理するんじゃなく Realm Migration 使う案

Realm が用意してくれてる Migration 案は Scheme の変更などで使うものであって、今回やりたいのは単なるデータの追加なので Migration ではないしな、と思って使っていない。

やったほうが良さそう

データ更新処理始める前にデータベースのバックアップ取っといたほうが良さそう。

おわり

結構いろいろ悩んだけど最終的にはシンプルな方法で解決できてよかった。

データ更新は裏で自動でやってもいいけど、変な操作されて壊れても困るし更新中だよって出すようにした。
更新中にアプリを kill したら次起動時にまた更新処理が走る。

f:id:star__hoshi:20171112014742g:plain