ここ一年位RealmSwiftをつかって、感じたこと(ぐち)まとめ
辛い中でマシな方法を模索した知見も書いてあります
概要
RealmSwiftを使うと、Swiftの言語で本来できるはずの様々なことができなくなります。辛いです。できないことばかり。
RealmSwiftを使うということは、Swiftではなく、RealmSwiftという環境でコーディングすることだと思ったほうがいい。
ORMapperの都合が多すぎて、全然ドメインロジックに集中させてもらえない。
じゃあ、Realmに更にオレオレWrapperライブラリを被せればいいのかということになるが、
- そんなことしている時点で、Realmを使う必要あるのか?
- RealmのObservingを利用できない
などの疑問がある。
正直、普通にSqliteのORMappingを使うか、ORMappingを自分で書いたほうが良いと思う。
(最近GRDB.swiftというものを使ったが、本当に最高だった。)
私がRealmを使った唯一と言っていいRealmのストロングポイントは、Remoteとのpush型のsyncが便利なこと。ただ、ゆるふわーな要件でのみの話。厳密なノード間のトランザクションは存在しない。エンタープライズ向けとは思えない。(確か2018年中盤のころの知識なので今はなにかあるかもしれない)
(今ならFirestoreの方がイケてると思います!)
以下辛いところ
スレッドを超えられない
managedObjだとスレッドを超えられない。よって、managedかunmanagedかを常に意識しなくてはいけない。
managed->unmanaged変換も浅い変換しかサポートされていない。
私はHydraというswiftでasync/await的なコードを書けるライブラリを愛用しているが、managedObjをその中で利用しようものならその瞬間クラッシュする。awaitはシングルスレッドじゃなくて、マルチスレッドだけど待ってくれる(コルーチンというらしいです)だけだから。
どんなに辛くても通常のRealmObjectとは別にunmanagedObj用のstructを書いて区別して使うことを強く推奨したい。
モデル定義
モデル定義:デフォルト値なしのletを定義
できない。辛い。
モデル定義:自由にコンストラクタを作れない
モデル定義:デフォルト値なしのletを定義にも関連するけど、絶対初期値を入れて欲しいときに、
class Dog: Object {
/// 空文字列と未定義の区別できない。開発者が常に脳みそのコストを払って注意すべし。
@objc dynamic name = ""
/// init()はnameが未定義に絶対使ってはならないというルールを開発者が常に脳みそのコストを払って徹底すること
convenience init(name: String) {
self.init()
self.name = name
}
}
辛い
モデル定義:インフラを意識した宣言が強制的に必要になる
@objc dynamic
←邪魔
モデル定義:自由にOptionalな基本型を定義できない
RealmOptional
←邪魔
モデル定義:非OptionalなObjectプロパティを定義できない
ValueObjectを扱えない(Structが扱えない)
RealmObjectをValueObjectとして利用しようとしたときの話です。
無理に扱おうとすると、きちんと責任を持ってOldObjectを削除しないとゴミだらけになる。というかそういうのを気にしなきゃいけないObjectはValueObjectではない。
やってられません。
なのでどんなに辛くてもRealmがサポートしているプリミティブ型(intとかstringとか)を流用して、決してRealmObjectクラスを作らないようにしなくてはなりません。
class Human: Object {
@objc dynamic var name: Name
}
// 本来はvalueObjectとして扱いたい
class Name: Object {
@objc dynamic var first = ""
@objc dynamic var last = ""
}
↓
class Human: Object {
@objc dynamic var first = ""
@objc dynamic var last = ""
var name: Name { return Name(first: first, last: last) }
}
struct Name {
let first: String
let last: String
}
Enumが扱えない
基本型のListが扱えない
- なんで
List<Int>
ってやらせてくれないん?
継承がサポートされていない
正確に言うと、SuperClassでの検索がサポートされていない
以下の様に、移譲で継承関係を表現しつつ頑張るしか無い。
class Animal: Object {
@objc dynamic isDead = true
func die() {
isDead = false
}
}
class Dog: Object {
var isDead: Bool { return super!.isDead }
@objc dynamic super: Animal?
func die () { super!.die() } // ← 残念なforcedUnwrap
func しっぽをふる() {
}
}
DownCastするはめになったら大変だ。TemplateMethodを実装しようと思うと大変だ。
ManagedObjectじゃないとObservingできない
いわゆるCRUDの画面で、RとUの画面の実装を統一しようとしたら詰む。
RealmのObservingはあくまで保存済みObjectのためのものらしい。
Updateの画面だと保存済みRealmObjに監視を効かせながら、textFieldの値を同期していればいい。でもCreateの画面だと、まだRealmに保存していないObjectを扱うことになり、そしてそれは監視できない。
ManagedObjectはwriteの中で、Unmanagedはwriteの外で変更しなくてはならない
↑ごめんなさい、久々に自分の記事を読み返して加湿修正しているのですが、書いた当時の私が何を言いたいか不明です
消しても子Objectが消えない
削除ロジックは自分で管理
ActiveRecordでいうdependency:destroyが恋しいですね。
RemoteとのTransactionはサポートされていない
RealmのtransactionはあくまでローカルのRealmFileへの書き込みに対してのみ。RealmFile同士のsyncにはTransactionもなにもない。
これでは、厳密な仕様を満たすことができない。書き込みDeviceを一つに絞るとか、書き込み用のサーバーを用意するとかしか思いつきません。
そして、何度も言っているように、そんなことをする羽目になっている時点でRealm使う意味あるのか?という疑問が生じます。
まとめ
ぐちばかりですが、逆に言うとこれらの辛みを認識して受け入れて、そして利用することで、Realmの様々な恩恵を受けることができます。
すべてを差し出した者にだけ、Realmの神様はホンモノの快適さを与えてくれるのです。
中途半端にフレームワークと喧嘩すると、双方の辛みを受けることになりかねないので、フレームワークとの喧嘩は慎重に…。