79
74

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-07-08

#目次

SwiftでRealmを使う時のTips まえがき
SwiftでRealmを使う時のTips(1) アクセサとエンティティ編
SwiftでRealmを使う時のTips(2) 生成とオートインクリメント編
SwiftでRealmを使う時のTips(3) NSPredicate編

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

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

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

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

NSPredicate は標準でいくつかのイニシャライザを持っています
RealmのNSPredicateチートシートを元に
下記のイニシャライザに条件文を突っ込むというやり方が一般的です

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

たとえばこんな感じ

let predicate = NSPredicate(format: "name = %@", argumentArray: ["Tom"])

これを、もし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)
    }
}

これだけ用意するのは面倒くさいもんですが
これにて使うときは下記のようにシンプルになります

// 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
    }
}

これで

// 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])
    }

こうしておけば

let cities = ["Tokyo", "Osaka", "Nagoya"]
let predicate = NSPredicate("city", valuesIn: cities)

このように直感的に

###BETWEEN句

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

NSPredicate+Initializers.swift
    public convenience init(_ property: String, between min: AnyObject, to max: AnyObject) {
        self.init(format: "\(property) BETWEEN {%@, %@}", argumentArray: [min, max])
    }

IntFloat もその他も吸収できるように最小値と最大値を AnyObject にしていますが
「整数しか使わねーよ」って方なら引数の型を Int とかにしてもいいかもしれません

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

// 年齢が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)
        }
    }
// 期限日が指定した日付までのデータを検索
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 に変換して扱います

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

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

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

#条件を結合する

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

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

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

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 を返します

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

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

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

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

79
74
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
79
74

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?