SwiftでRealmを使う時のTips(3) NSPredicate編

#目次

[SwiftでRealmを使う時のTips まえがき](http://qiita.com/nakau1/items/a591d9cc34e5b575ceb5)
[SwiftでRealmを使う時のTips(1) アクセサとエンティティ編](http://qiita.com/nakau1/items/21ed81e7f38c104f760e)
[SwiftでRealmを使う時のTips(2) 生成とオートインクリメント編](http://qiita.com/nakau1/items/0323807abed795a43393)
[SwiftでRealmを使う時のTips(3) NSPredicate編](http://qiita.com/nakau1/items/40865299dacc50d71604)

#NSPredicateについて
swiftを使ったiOS開発にて `Realm` を扱う時によく使用するのが
`NSPredicate` クラスです

英語で「predicate」とは「述語」という意味らしいですが
SQL構文でいうところの `WHERE文` とニアリーイコールと考えて相違ないかと思います

ですので、ここでは「`NSPredicate` = 条件」という言葉を使っていこうと思います
この「条件オブジェクト」を組み合わせて、`Realm` に問い合わせを行う感じですね

#NSPredicateのイニシャライザを拡張する

`NSPredicate` は標準でいくつかのイニシャライザを持っています
[RealmのNSPredicateチートシート](https://realm.io/news/nspredicate-cheatsheet/)を元に
下記のイニシャライザに条件文を突っ込むというやり方が一般的です

```swift
public /*not inherited*/ init(format predicateFormat: String, argumentArray arguments: [AnyObject]?)
```

たとえばこんな感じ

```swift
let predicate = NSPredicate(format: "name = %@", argumentArray: ["Tom"])
```
これを、もしSQL文で表現するとすればこんな感じになりますね

```sql
WHERE name = 'Tom'
```

ただ、「文字列を平文で打つの?」って感じがあります
なんとなくイケてない感じがしてしまいます

なので、自分は `NSPredicate` のイニシャライザを拡張する手をよく使います

```NSPredicate+Initializers.swift
public extension NSPredicate {
    
    private convenience init(expression property: String, _ operation: String, _ value: AnyObject) {
        self.init(format: "\(property) \(operation) %@", argumentArray: [value])
    }
}
```
まず `private` なコンビニエンスイニシャライザを用意しました

で、これを元にさらに拡張します

```NSPredicate+Initializers.swift
public extension NSPredicate {
    
    public convenience init(_ property: String, equal value: AnyObject) {
        self.init(expression: property, "=", value)
    }
    
    public convenience init(_ property: String, notEqual value: AnyObject) {
        self.init(expression: property, "!=", value)
    }
    
    public convenience init(_ property: String, equalOrGreaterThan value: AnyObject) {
        self.init(expression: property, ">=", value)
    }
    
    public convenience init(_ property: String, equalOrLessThan value: AnyObject) {
        self.init(expression: property, "<=", value)
    }
    
    public convenience init(_ property: String, greaterThan value: AnyObject) {
        self.init(expression: property, ">", value)
    }
    
    public convenience init(_ property: String, lessThan value: AnyObject) {
        self.init(expression: property, "<", value)
    }
}
```
これだけ用意するのは面倒くさいもんですが
これにて使うときは下記のようにシンプルになります

```swift
// Tomである
let predicate = NSPredicate("name", equal: "Tom")

// Tomじゃなぁ〜い
let predicate = NSPredicate("name", notEqual: "Tom")
```

##文字列検索

`Realm` は当然文字列を検索する能力もあります
一般的なSQLではそんな時は `LIKE句` を使うわけですが
`Realm` では `LIKE` はサポートしてません (なんで?)
代わりに何種類かの予約語があります

```NSPredicate+Initializers.swift
public extension NSPredicate {
    // 前後方一致検索(いわゆる、あいまい検索)
    public convenience init(_ property: String, contains q: String) {
        self.init(format: "\(property) CONTAINS '\(q)'")
    }

    // 前方一致検索
    public convenience init(_ property: String, beginsWith q: String) {
        self.init(format: "\(property) BEGINSWITH '\(q)'")
    }

    // 後方一致検索
    public convenience init(_ property: String, endsWith q: String) {
        self.init(format: "\(property) ENDSWITH '\(q)'")
    }
}
```

ここまで用意して、いざ使おうとすると問題が発生してしまいます
バックスラッシュとシングルクォートを含んだ文字列を渡すと
アプリがクラッシュするのです

なんだか "データベースあるある" ですね
ちゃんとエスケープをしておいたほうが懸命なようです

なので、今度は文字列に対して拡張を行います

```String+Realm.swift
public extension String {
    
    /// Realm用にエスケープした文字列
    public var realmEscaped: String {
        let reps = [
            "\\" : "\\\\",
            "'"  : "\\'",
            ]
        var ret = self
        for rep in reps {
            ret = self.stringByReplacingOccurrencesOfString(rep.0, withString: rep.1)
        }
        return ret
    }
}
```

これで

```swift
// Tomって文字がどこかにあるかなぁー?
let predicate = NSPredicate("name", contains: "Tom")

// Tom's Shopって文字がどこかにあるかなぁー?
let predicate = NSPredicate("name", contains: "Tom's Shop".realmEscaped)
```

文字列検索ができるようになりました

##その他の検索
###IN句

SQLでよくある `IN句` も `Realm` にはあります
`IN句` の場合はフォーマットに対して配列を配列にして渡す必要があります

当たり前といえば当たり前なのですが、これが意外とミスりやすいです
なので、これも分かりやすく下記のようにラップしておくのがよいかと思います

```NSPredicate+Initializers.swift
    public convenience init(_ property: String, valuesIn values: [AnyObject]) {
        self.init(format: "\(property) IN %@", argumentArray: [values])
    }
```
こうしておけば

```swift
let cities = ["Tokyo", "Osaka", "Nagoya"]
let predicate = NSPredicate("city", valuesIn: cities)
```
このように直感的に

###BETWEEN句

`BETWEEN句` も `Realm` はサポートされてます
`Realm` の `BETWEEN句` の作法は「ブレスで囲む」ということのようですが
これも忘れがちというか間違いやすいので下記のようにラップしておくのがよいかと思います

```NSPredicate+Initializers.swift
    public convenience init(_ property: String, between min: AnyObject, to max: AnyObject) {
        self.init(format: "\(property) BETWEEN {%@, %@}", argumentArray: [min, max])
    }
```
`Int` も `Float` もその他も吸収できるように最小値と最大値を `AnyObject` にしていますが
「整数しか使わねーよ」って方なら引数の型を `Int` とかにしてもいいかもしれません

下記のように文法作法を覚えなくても直感的になりました

```swift
// 年齢が18歳から40歳まで
let predicate = NSPredicate("age", between: 18, to: 40)
```

###日付のFromTo

データベースを使うアプリでよく出てくるのは、
「いつからいつまで」という日付範囲を指定しての検索ではないでしょうか

「いつから」だけの検索も、「いつまで」だけの検索も、その両方も
ひとつのメソッドでできるとありがたいなぁということで、
`BETWEEN` にはせずに下記のように、ちょっと長めのメソッドとあいなりました

```NSPredicate+Initializers.swift
    public convenience init(_ property: String, fromDate: NSDate?, toDate: NSDate?) {
        var format = "", args = [AnyObject]()
        if let from = fromDate {
            format += "\(property) >= %@"
            args.append(from)
        }
        if let to = toDate {
            if !format.isEmpty {
                format += " AND "
            }
            format += "\(property) <= %@"
            args.append(to)
        }
        if !args.isEmpty {
            self.init(format: format, argumentArray: args)
        } else {
            self.init(value: true)
        }
    }
```

```swift
// 期限日が指定した日付までのデータを検索
let date: NSDate = /* NSDateを作る処理(割愛) */
let predicate = NSPredicate("expiredDate", fromDate: nil, toDate: date)

// 購入日が指定した日付からのデータを検索
let date: NSDate = /* NSDateを作る処理(割愛) */
let predicate = NSPredicate("purchasedDate", fromDate: date, toDate: nil)

// 予約日が指定した日付1から日付2までのデータを検索
let date1: NSDate = /* NSDateを作る処理(割愛) */
let date2: NSDate = /* NSDateを作る処理(割愛) */
let predicate = NSPredicate("reservedDate", fromDate: date1, toDate: date2)
```

###ID検索

ここまでのイニシャライザは、チートチャートを元に
標準的な方法をラップするものでした

ここでは前回までに作った `NBRealmEntity` クラスに特化したイニシャライザを用意します

主キーであるidを検索条件に簡単にできるようにすれば色々と便利です

```NSPredicate+Initializers.swift
public extension NSPredicate {
    
    public convenience init(ids: [Int64]) {
        let arr = ids.map { NSNumber(longLong: $0) }
        self.init(format: "\(NBRealmEntity.IDKey) IN %@", argumentArray: [arr])
    }
    
    public convenience init(id: Int64) {
        self.init(format: "\(NBRealmEntity.IDKey) = %@", argumentArray: [NSNumber(longLong: id)])
    }
}
```

 `AnyObject` には `Int64` をそのままつっこめないので
 `NSNumber` に変換して扱います

そんな変換処理を毎回やってられないので
このようなコンビニエンスイニシャライザを用意しておくのがいいですね

```swift
// ID=32のオブジェクト
let predicate = NSPredicate(id: 32)

let predicate = NSPredicate(ids: [12, 23, 27, 39])
```

#条件を結合する

SQLの `WHERE文` は、ひとつの条件だけで収まることは、なかなか無いことです
たいていは複数の条件を組み合わせてデータを抽出します

```sql
/* 例) 東京以外の出身で20歳の男性をユーザテーブルから検索 */
SELECT
    *
FROM
    users
WHERE
    gender = "man" AND
    age = 20 AND
    birth_place != "tokyo"
```

これを `NSPredicate` では
`NSCompoundPredicate` という `NSPredicate` の継承クラスを用いて実現させます

```swift
let predicates = [
    NSPredicate(format: "gender = %@", argumentArray: ["man"]),
    NSPredicate(format: "age = %@", argumentArray: [20]),
    NSPredicate(format: "birthPlace != %@", argumentArray: ["tokyo"]),
]
let compoundedPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
```

やってることは分かりますが、なかなかややこしいですね
`AND` で繋ぐだけならまだシンプルですが、`OR` などが入るとさらにややこしいです

ここもシンプルに実装できるように改造しました

```NSPredicate+Compound.swift
public extension NSPredicate {

    public func compound(predicates: [NSPredicate], type: NSCompoundPredicateType = .AndPredicateType) -> NSPredicate {
        var p = predicates; p.insert(self, atIndex: 0)
        switch type {
        case .AndPredicateType: return NSCompoundPredicate(andPredicateWithSubpredicates: p)
        case .OrPredicateType:  return NSCompoundPredicate(orPredicateWithSubpredicates:  p)
        case .NotPredicateType: return NSCompoundPredicate(notPredicateWithSubpredicate:  self.compound(p))
        }
    }
}
```

まずこのようなメソッドを `NSPredicate` に足すことで
`NSCompoundPredicate` の長ったらしいイニシャライザをシンプルにします

で、さらにこいつをラップするものを作りました

```NSPredicate+Compound.swift
public extension NSPredicate {
    
    public func and(predicate: NSPredicate) -> NSPredicate {
        return self.compound([predicate], type: .AndPredicateType)
    }
    
    public func or(predicate: NSPredicate) -> NSPredicate {
        return self.compound([predicate], type: .OrPredicateType)
    }
    
    public func not(predicate: NSPredicate) -> NSPredicate {
        return self.compound([predicate], type: .NotPredicateType)
    }
}
```

`compound()` メソッドが結合させた `NSPredicate` を返していることから
これらはチェーンして繋いでいくことができます

その前に一個、個人的な好みなのですが
下記のような読み取り専用のプロパティをこしらえておきました

```NSPredicate+Compound.swift
public extension NSPredicate {
    
    public static var empty: NSPredicate {
        return NSPredicate(value: true)
    }
}
```

これは絶対に条件が `true` になる `NSPredicate` を返します

で、今まで作ったイニシャライザと、先ほどのものと組み合わてみると

```swift
// After
let predicates = NSPredicate.empty
    .and(NSPredicate("gender",     equal:    "man"))
    .and(NSPredicate("age",        equal:    20))
    .and(NSPredicate("birthPlace", notEqual: "tokyo"))
```

なんということでしょう・・・・・

行数こそあまり変わりませんが、SQLライクな感じで分かりやすくなったかと思います