はじめに
僕が開発しているiOSアプリではデータの保存にRealmを使用しています。とても便利なライブラリなのですがスキーマの管理をしっかりしないと使えなくなったり書き方次第ではクラッシュしまくりのアプリになってしまいます。
例えば、
let realm = try! Realm()
のようにRealmオブジェクトのインスタンス化する際に、オブジェクトのプロパティを追加してたりすると、
Error Domain=io.realm Code=10 "Migration is required due to the following errors:
- Property 'hoge.hoge' has been added." UserInfo={NSLocalizedDescription=Migration is required due to the following errors
みたいなかんじでエラーがはかれるかと思います。
この場合の対処は、開発環境であればいっかいアンインストールして実行しなおすか、Realm.Configurationにスキーマの設定をすることで解決することができます。
アプリとして運用を考えている場合はconfigをしっかり管理することが重要ですね。
事件発生
僕らの開発しているプロジェクトではRealmオブジェクトをインスタンス化する際に
let realm = try! Realm()
と呼ぶようになっていまして、エラーが発生した場合はクラッシュするようになっていました。
僕が開発に加わる前からこのようなコードになっていて、エラー処理をしっかりわけないとなと思いつつも、
Realm.Configurationでちゃんとスキーマの管理をしてるし、そもそもそこでエラーが起きてしまった際にはもうそのユーザーはRealmで実装されてる機能が使えなくなってしまうのでここでエラーが発生しないように務めるのが大事だなとおもったので(あと見て見ぬふりをしていました、、)そのままにしていました。
しかし、開発環境で大丈夫だと思って申請も通ってバージョンアップしたものが、一部ユーザーでエラーが発生していたのがあとになって発覚するという一大事件が発生しました。
おそらく中途半端にけしたオブジェクトが残ってしまっていたユーザーに対して発生してしまったのではないかなと思われます。
その際にRealm絡みのところを安全に使えるようにいろいろ書き換えたのでまとめようと思います。
データリセット
一番肝になるかなと思った部分が、もしRealmオブジェクトを生成するときのエラーが発生してしまったときにアップデートを挟むか、そのユーザーがアンインストールしてインストールし直す挙動を取ってもらわないと使えなくなってしまうので、そのような自体が発生したときの対処だと思いました。
そのためにはエラーが発生したときにRealmに保存されていたデータをリセットするという挙動をとることにしました。
そこでいろいろ調べた際に、Realmのデータを保存しているファイルを削除すれば大丈夫という記事を発見したのでそれをまず試してみました。
保存先のファイルパスは、
Realm.Configuration.fileURL
で取得できるので、
try FileManager.default.removeItem(atPath: {ファイルパス})
で削除できると思いきやうまく行きませんでした。なので別の方法を調べていたら、そもそもRealmにそのような機能が実装されていたのでそれを使用すればいいだけでした。
deleteRealmIfMigrationNeeded
Realm.ConfigurationにdeleteRealmIfMigrationNeededというプロパティが用意されているのでインスタンス化する際に、
var config = Realm.Configuration
config.deleteRealmIfMigrationNeeded = true
let realm = try! Realm(configuration: config)
のように設定することで、既に保存されているものをリセットした上でRealmオブジェクトが生成されます。
実際に書いたコード
なるほど、便利機能じゃんって思いましたが、そもそも万が一に備えてのリセットなので万が一の場合のみ適用されるような実装にしました。以下のように実装しました。
import RealmSwift
protocol RealmUsable {
static var filePath: String { get }
static var config: Realm.Configuration { get }
static var schema: Realm.Configuration { get }
static func createRealm() -> Realm?
static func addFileURL(config: Realm.Configuration) -> Realm.Configuration
}
extension RealmUsable {
static var config: Realm.Configuration {
var c = schema
c.fileURL = c.fileURL?
.deletingLastPathComponent()
.appendingPathComponent(filePath)
.appendingPathExtension("realm")
return c
}
static var schema: Realm.Configuration {
return Realm.Configuration(schemaVersion: 1, migrationBlock: { migration, oldSchemeVersion in
if oldSchemeVersion < 1 {
︙
}
})
}
static func createRealm() -> Realm? {
do {
return try Realm(configuration: config)
} catch let error as NSError {
assertionFailure("realm error: \(error)")
var config = self.config
config.deleteRealmIfMigrationNeeded = true
return try? Realm(configuration: config)
}
}
}
このように実装しました。
RealmUsableというプロトコルを作成し、Realmを使用するClassで採用するようにすることでRealmオブジェクト生成の部分とスキーマの管理を一元化できるようにしました。
createRealm()では、Realmオブジェクトの生成が失敗した際にcatchに入って、開発環境であればassertionFailure(“realm error: (error)”)の部分でクラッシュするようにしていてスキーマの管理がミスってたときに気づきやすいようにしています。そして本番環境であればデータをリセットしてオブジェクトを返すようにしています。ここでは返り値をRealm?にしていますが、最後のRealmオブジェクトの生成が失敗した時を考慮するかどうかでわけたほうがいいと思います。
おわりに
今まで放置してたせいで管理しにくい構造な上にそれでちゃんとエラーにならないように慎重にやっていたので余計な工数を食っていたのがここでちゃんと構造を整えたことでだいぶ開発がスムーズになりました。参考になれば幸いです。