#目次
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
のイニシャライザを拡張する手をよく使います
public extension NSPredicate {
private convenience init(expression property: String, _ operation: String, _ value: AnyObject) {
self.init(format: "\(property) \(operation) %@", argumentArray: [value])
}
}
まず private
なコンビニエンスイニシャライザを用意しました
で、これを元にさらに拡張します
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
はサポートしてません (なんで?)
代わりに何種類かの予約語があります
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)'")
}
}
ここまで用意して、いざ使おうとすると問題が発生してしまいます
バックスラッシュとシングルクォートを含んだ文字列を渡すと
アプリがクラッシュするのです
なんだか "データベースあるある" ですね
ちゃんとエスケープをしておいたほうが懸命なようです
なので、今度は文字列に対して拡張を行います
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句
の場合はフォーマットに対して配列を配列にして渡す必要があります
当たり前といえば当たり前なのですが、これが意外とミスりやすいです
なので、これも分かりやすく下記のようにラップしておくのがよいかと思います
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
はサポートされてます
Realm
の BETWEEN句
の作法は「ブレスで囲む」ということのようですが
これも忘れがちというか間違いやすいので下記のようにラップしておくのがよいかと思います
public convenience init(_ property: String, between min: AnyObject, to max: AnyObject) {
self.init(format: "\(property) BETWEEN {%@, %@}", argumentArray: [min, max])
}
Int
も Float
もその他も吸収できるように最小値と最大値を AnyObject
にしていますが
「整数しか使わねーよ」って方なら引数の型を Int
とかにしてもいいかもしれません
下記のように文法作法を覚えなくても直感的になりました
// 年齢が18歳から40歳まで
let predicate = NSPredicate("age", between: 18, to: 40)
###日付のFromTo
データベースを使うアプリでよく出てくるのは、
「いつからいつまで」という日付範囲を指定しての検索ではないでしょうか
「いつから」だけの検索も、「いつまで」だけの検索も、その両方も
ひとつのメソッドでできるとありがたいなぁということで、
BETWEEN
にはせずに下記のように、ちょっと長めのメソッドとあいなりました
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を検索条件に簡単にできるようにすれば色々と便利です
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
などが入るとさらにややこしいです
ここもシンプルに実装できるように改造しました
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
の長ったらしいイニシャライザをシンプルにします
で、さらにこいつをラップするものを作りました
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
を返していることから
これらはチェーンして繋いでいくことができます
その前に一個、個人的な好みなのですが
下記のような読み取り専用のプロパティをこしらえておきました
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ライクな感じで分かりやすくなったかと思います