クエリのwhere節での記法の注意 〜NSPredicateを意識して書くべし〜【RealmSwift】
問題
final class Page: Object {
@Persisted(primaryKey: true) var id = UUID().uuidString
@Persisted var title: String
@Persisted var isMarked = false
このようなエンティティ定義があったとして、
let pageList = List<Page>(...)
let searchText = "banana"
let results = pageList.where {
$0.isMarked
&& $0.title.contains(searchText, options: .caseInsensitive)
}
このようなクエリを書くと、実行時にエラーが出て怒られます。
解決策
where
節の中でBool
型の変数を使うときには、boolValue
ではなくboolValue == true
というように記述するようにします。
つまり上の例では、
pageList.where {
$0.isMarked == true // ここ
&& $0.title.contains(searchText, options: .caseInsensitive)
}
とするのが正しいです。
理由
色々と実験をすることで、このようなことがわかりました。
-
where
節に指定した、クエリを表すクロージャは、その通りに実行されるわけではない - クエリは内部的に
NSPredicate
の構文に変換され、バリデーションはNSPredicate
を用いて実行される。
まず、where
節の定義を見てみましょう。
func `where`(_ isIncluded: ((Query<Element>) -> Query<Bool>)) -> Results<Element>
お分かりの通り、クエリのクロージャの型は、((Element) -> Bool)
ではなく((Query<Element>) -> Query<Bool>))
となっています。このことから、どうやらトリックがありそうです。
次に、最初に紹介した誤ったクエリを実行したときの実行時エラーのメッセージを見てみましょう。
Unable to parse the format string \"(isMarked && (title CONTAINS[c] %@))\"
「NSPredicate
のフォーマット文字列をパースできない」という内容なので、NSPredicate
から発せられた例外でしょう。しかし書いたコードではそんなものを使っていません。よって、クエリのクロージャは、何らかの方法でNSPredicateに翻訳されていると考えるのが妥当でしょう。つまり、クエリのクロージャは、それぞれの要素をバリデートをするたびに実行されているわけではなく、同じ意味のNSPredicate
に変換されてから実行されるのです。
クエリを型安全に書けたのは、Query
に@dynamicMemberLookup
属性が付いていることによるものです。クエリの中で書いた$0.title
というのは、実際にPage
オブジェクトのtitle
プロパティにアクセスしているわけではありません。
$0.isMarked == true
と$0.isMarked
の両者とも、型の整合性は取れているので、後者のような誤った書き方でもコンパイルができてしまいます。よってNSPredicate
に渡される表現は構文として不正で、実行時エラーが発生するのです。
つまり、コンパイラによってクエリの型安全は保証されますが、構文としての妥当性は保証されていません。以上のことに気をつけながら、クエリを書きましょう。
参考記事
環境
- realm-swift v10.25.0
- Xcode 13.3
- macOS 12.3
- iOS 15.4
Discussion