SQLite/CoreDataをリプレースする可能性のあるデータストレージRealm
をそれなりに使ってみたので基本的な使い方含めてまとめてみます。あんまり見やすくなくてスイマセン後日校正します。
cocoapods経由でのインストール
gem install cocoapods --pre
platform :ios, '8.0'
source 'https://github.com/CocoaPods/Specs.git'
pod 'Realm'
> pod install
Analyzing dependencies
Downloading dependencies
Installing Realm (0.90.5)
Generating Pods project
Integrating client project
[!] From now on use `sample001.xcworkspace`.
以下のBridging-Headerを追加
sample001-Bridging-Header.h
//
// sample001-Bridging-Header.h
// sample001
//
// Created by Takatomo Okitsu on 2015/02/12.
// Copyright (c) 2015年 Takatomo Okitsu. All rights reserved.
//
#ifndef sample001_sample001_Bridging_Header_h
#define sample001_sample001_Bridging_Header_h
#import <Realm/Realm.h>
#endif
Swift/RLMSupport.swift をXcodeのナビゲータエリアにドラッグ&ドロップ
データベースの作成
- Realmは、データベースファイルを分割したいとかファイルパスを明示的に指定したい等なければ、 特に初期化処理は不要。
- 以下はデータベースを示すRLMRealmオブジェクトの初期化方法をいくつか
// デフォルトのファイルを利用する場合
let realm = RLMRealm.defaultRealm()
// ファイルパスを変更する場合
RLMRealm.setDefaultRealmPath("app.realm")
let realm = RLMRealm.defaultRealm()
// データベースファイルを指定して利用する場合
let realm2 = RLMRealm(path: "app2.realm")
// インメモリで利用する場合(永続化しない)
let realmInmemory = RLMRealm.inMemoryRealmWithIdentifier("inMemory")
- 基本的には
RLMRealm.defaultRealm()
を利用するで問題ないはずだが、iOS8のSharedContainerなどでアプリケーションをまたいで利用する要な場合にはファイルパスの変更は役に立つ。(本家マニュアルより)
モデルクラス
- モデルクラスは、
RLMObject
クラスのサブクラスを作るのみ。
User.swift
class User: RLMObject {
dynamic var id = ""
dynamic var name = ""
dynamic var createdAt:Double = 0
}
プライマリキーの追加
- プライマリキーは、以下のようにprimaryKey()メソッドをoverrideすることで定義できる。
User.swift
class User: RLMObject {
dynamic var id = ""
dynamic var name = ""
dynamic var createdAt:Double = 0
override class func primaryKey() -> String {
return "id"
}
}
- Realmでは、CoreDataと違いプライマリキーを定義できる。
- CoreDataではAtomicに保存するのが一苦労だったが、その点Realmはフレームワークが用意してくれているので楽。
- createdAtをDoubleで定義している理由は、NSDate型はミリ秒が丸められてしまうため、ミリ秒まで必要な場合は、NSTimeInterval(Double)型で管理する必要がある。https://github.com/realm/realm-cocoa/issues/875
インデックスの追加
- インデックスの概念も存在し、
attributesForProperty
メソッドをoverrideすることで定義できる。(ベンチマークは後半参照)
User.swift
class User: RLMObject {
dynamic var id = ""
dynamic var name = ""
dynamic var createdAt:Double = 0
override class func primaryKey() -> String {
return "id"
}
override class func attributesForProperty(propertyName: String!) -> RLMPropertyAttributes {
var attributes = super.attributesForProperty(propertyName)
if propertyName == "id" {
attributes |= RLMPropertyAttributes.AttributeIndexed
}
return attributes
}
}
デフォルト値の追加
-
defaultPropertyValues
をoverrideすることで定義できる。ただ、プロパティにデフォルト値を定義しても同じなんじゃないかなという気がする誰か教えて。
class User: RLMObject {
dynamic var id = ""
dynamic var name = ""
dynamic var createdAt:Double = 0
override class func primaryKey() -> String {
return "id"
}
override class func attributesForProperty(propertyName: String!) -> RLMPropertyAttributes {
var attributes = super.attributesForProperty(propertyName)
if propertyName == "id" {
attributes |= RLMPropertyAttributes.AttributeIndexed
}
return attributes
}
override class func defaultPropertyValues() -> [NSObject : AnyObject]! {
return [
"name": "",
"createdAt": 0
]
}
}
オブジェクトの追加・更新・参照など
追加
- 以下のようにトランザクションを明示的に指定して追加する必要がある。更新に関しても同様。
let user = User()
user.id = "1"
user.name = "Takatomo Okitsu"
user.createdAt = NSDate().timeIntervalSince1970
let realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction() //ここから開始
realm.addObject(user)
realm.commitWriteTransaction() //終了
- クロージャを利用した書き方もできる。個人的にはこっちのほうが好み。
// クロージャを利用した書き方
let user = User()
user.id = "1"
user.name = "Takatomo Okitsu"
user.createdAt = NSDate().timeIntervalSince1970
let realm = RLMRealm.defaultRealm()
realm.transactionWithBlock({ () -> Void in
realm.addObject(user)
})
- プライマリキーが重複している要な場合には、以下の様な例外が発生する。
2015-02-17 19:36:04.981 sample001[8351:298069] *** Terminating app due to uncaught exception 'RLMException', reason: 'Can't set primary key property 'id' to existing value '1'.'
-
addOrUpdateObject
メソッドを利用することで、すでにプライマリキーが同じオブジェクトが存在している場合には、新しいオブジェクトに置き換わる。この場合は当然例外は発生しない。(MySQLのREPLACE構文と同じ扱い)
let user = User()
user.id = "1"
user.name = "kenta tanaka"
user.createdAt = NSDate().timeIntervalSince1970
let realm = RLMRealm.defaultRealm()
realm.transactionWithBlock({ () -> Void in
realm.addOrUpdateObject(user) //IDが重複していた場合は、新しいオブジェクトで置き換える
})
-
createInDefaultRealmWithObject
もしくは、createOrUpdateInDefaultRealmWithObject
を利用することで、Dictionaryから簡単にオブジェクトを追加することもできる。 -
createInDefaultRealmWithObject
は、すでにオブジェクトが存在する場合は例外を出力する。createOrUpdateInDefaultRealmWithObject
は、指定されたプロパティを上書きする( オブジェクトの置き換えではない。これ重要。上で紹介したaddOrUpdateObjectと挙動が異なるので注意 ) - いずれもプライマリキーの指定は必須で、かつトランザクション内で実行する必要がある。
// createInDefaultRealmWithObject
let userDict = [
"id": "1",
"age": 34,
"createdAt": NSDate().timeIntervalSince1970
]
realm.transactionWithBlock({ () -> Void in
let user = User.createInDefaultRealmWithObject(userDict)
//既に存在する場合は例外
})
// createOrUpdateInDefaultRealmWithObject
let userDict = [
"id": "1",
"age": 34,
"createdAt": NSDate().timeIntervalSince1970
]
realm.transactionWithBlock({ () -> Void in
let user = User.createOrUpdateInDefaultRealmWithObject(userDict)
//既に存在する場合は、指定したプロパティを上書き
})
更新
- トランザクションの中でプロパティを更新することで更新が可能。
realm.transactionWithBlock({ () -> Void in
user.name = "takashi tanaka"
})
- トランザクション外でプロパティを更新すると、例外が発生。 これ重要
user.name = "takashi tanaka" // ここで例外
realm.transactionWithBlock({ () -> Void in
user.createdAt = NSDate().timeIntervalSince1970
})
2015-02-19 13:43:30.868 sample001[1506:26987] *** Terminating app due to uncaught exception 'RLMException', reason: 'Attempting to modify object outside of a write transaction - call beginWriteTransaction on an RLMRealm instance first.'
- 追加・更新処理は同期的に行われるので、その間は他の処理をブロックすることになる。
- ブロックしたくない場合は、別スレッドで処理するなどの工夫を自前で行う。
- トランザクションがコミットされた時点で、他のスレッドにも自動的にその変更が適用される(CoreDataのようにコンテキストをマージするようなことは不要)
参照(クエリ)
1件取得
// プライマリキーで取得
let user = User(forPrimaryKey: "1") // Userオブジェクトが返却。存在しない場合はnil
複数件取得
- 複数件取得の場合は、戻り値は
RLMObject
を含むRLMResults
が返却される。
// 全件取得
let users = User.allObjects()
// Predicateを使ったフェッチ
let users = User.objectsWhere("name == Takatomo Okitsu")
// プレースホルダー
let users = User.objectsWhere("name CONTAINS %@", "okitsu")
// ユーザオブジェクト(RLMObject)へのアクセスの仕方
for user in users {
println("user.name: \(user.name)")
}
- RLMResultsは、NSFastEnumerationプロトコルに準拠しているので、for ... in 構文で使うことができる。
- 使える構文は、公式のリファレンス参照 http://realm.io/jp/docs/cocoa/0.90.5/
メソッドチェーン
- RLMResultsは以下の様なメソッドチェーンが使えて便利
// 取得したユーザオブジェクト一覧をソート
let users = User.allObjects().sortedResultsUsingProperty("createdAt", ascending: false)
//取得したユーザオブジェクトから、更にPredicateで絞り込んで、ソート
let users = User.allObjects().objectsWhere("age > 20").sortedResultsUsingProperty("createdAt", ascending: false)
最小値、最大値、平均値、合計
let users = User.allObjects()
// 最小値
let min = users.minOfProperty("age")
// 最大値
let max = users.maxOfProperty("age")
// 合計値
let sum = users.sumOfProperty("age")
// 平均値
let average = users.averageOfProperty("age")
リレーションシップ
1対多
- Twitterみたいに、ユーザがツイート(Status)を複数持つような構成を想定してみる
User.swift
class User: RLMObject {
dynamic var id = ""
dynamic var name = ""
dynamic var createdAt:Double = 0
dynamic var statuses = RLMArray(objectClassName: Status.className()) // Statusオブジェクトを複数保持
...
}
Status.swift
class Status: RLMObject {
dynamic var id = ""
dynamic var text = ""
dynamic var createdAt:Double = 0
...
}
- UserオブジェクトにStatusオブジェクトを追加する方法は以下の通り(トランザクション外でaddObjectすると例外発生するので注意)
let user = User(forPrimaryKey: "1")
let status:Status = Status()
status.id = "1"
status.text = "what are you doing?"
status.createdAt = NSDate().timeIntervalSince1970
realm.transactionWithBlock({ () -> Void in
user.statuses.addObject(status)
//これもトランザクション外だと例外発生するので注意
})
Inverse Relationship
- 逆引き。XCodeでいうところの以下
class Status: RLMObject {
dynamic var id = ""
dynamic var text = ""
dynamic var user:User? {
return linkingObjectsOfClass("User", forProperty: "statuses").first as? User // これ
}
dynamic var createdAt = 0
}
- 上記のように設定することで、
let owner = status.user
と呼び出すことができる。
Notification(通知)
- 以下のように指定することで、データに更新がかかるたびに通知が受け取れる。
let realm = RLMRealm.defaultRealm()
self.token = realm.addNotificationBlock { note, realm in
self.tableView.reloadData()
}
- どのオブジェクトに更新がかかったかまでは特定できない。(このあたりはCoreData+FetchedResultsControllerのほうが便利)
- issueにはあがっている https://github.com/realm/realm-cocoa/issues/601
- FetchedResultsController風に使えるようにしたライブラリもあったり。 RBQFetchedResultsController
マイグレーション
- オブジェクトやパラメータが追加された場合、何もしないと例外が出力されてアプリが起動不可になる。
- 変更があった場合には、スキーマバージョンをインクリメントして、Realmに教えて上げる必要がある。以下はdidFinishLaunchingWithOptions内のどこかで実行しておく。
- 単なるオブジェクトやパラメータ追加等であれば、インクリメントして終わり。もしデータの置換等が必要であれば、ブロックの中に処理を追加する。
// スキーマバージョンをインクリメントする。(1 -> 2)
RLMRealm.setSchemaVersion(2, forRealmAtPath: RLMRealm.defaultRealmPath(),
withMigrationBlock: { migration, oldSchemaVersion in
// 必要に応じて、処理を行う。(例えばデフォルト値いれるとか)
if oldSchemaVersion < 2 {
}
})
- マイグレーションのサンプルソースはこちら
RLMResultsが便利
- 簡単に言うと、FetchedResultsControllerのような使い方ができる。
- RLMResultsのインスタンスは、常に永続化されたデータの最新の状態を参照できる。
- Notificationと組み合わせると以下の様な使い方が可能。
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
var notificationToken: RLMNotificationToken?
var results:RLMResults?
override func viewDidLoad() {
super.viewDidLoad()
// RLMResultsオブジェクトをメンバ変数に保持
self.results = Status.allObjects().sortedResultsUsingProperty("createdAt", ascending: false)
self.tableView.reloadData()
notificationToken = RLMRealm.defaultRealm().addNotificationBlock { note, realm in
//ここで通知を受けたタイミングで、self.results オブジェクトは最新の状態を参照できるようになっている。
//そのため、tableViewであればreloadDataすることで最新の状態に更新される。
self.tableView.reloadData()
}
}
- ただし、FetchedResultsControllerのように、変更された行(オブジェクト)は通知されない。(本家では現在実装中の模様)
スレッド
- 先に言うと、CoreData同様、Realmの世界でもスレッドの扱いは面倒。
- Realmを使ったからといって、スレッドをまたぐオブジェクトの扱いはそれほど楽にはならない。(これ重要)
Realmにおけるバックグラウンドでの保存
- バックグラウンドで100件オブジェクトを保存する例
let queue = dispatch_queue_create("com.okitsutakatomo.background.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, { () -> Void in
let realm = RLMRealm.defaultRealm()
realm.transactionWithBlock({ () -> Void in
for i in 1...100 {
let user = User()
user.id = "\(i)"
user.name = "Takatomo Okitsu"
user.age = i
user.createdAt = NSDate().timeIntervalSince1970
realm.addObject(user)
}
})
})
- トランザクションブロックを抜けたタイミングで、すべてのスレッドに更新はかかる。(CoreDataのように、明示的にマージ指示を行う必要はなし。)
- この間、別スレッドからの参照、更新は問題なく行える。
スレッド間でのオブジェクトの移動
- スレッド間でのオブジェクトの移動は不可。移動しようとすると以下の例外が発生する。
2015-02-20 15:32:14.434 sample001[13950:512596] *** Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread'
- スレッド間でオブジェクトの移動は不可のため、ベタにプライマリキーを事前に変数化しておいて、スレッド内で取得する必要あり。(これ良い方法あれば誰かご教授ください)
let userId = user.id
let queue = dispatch_queue_create("com.okitsutakatomo.background.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, { () -> Void in
let user = User(forPrimaryKey: userId)
})
- スレッドをまたぐ方法に関しては、現在公式でbackground-queryという機能を実装中であり、これが完成するとスレッド間でのオブジェクトの受け渡しができるようになる見込み。 https://twitter.com/realm/status/567692214240047104?lang=ja
Realmその他大事な注意事項
- NSDate型は、ミリ秒が丸められてしまうため、ミリ秒まで必要な場合は、NSTimeInterval(Double)型で管理する必要がある。https://github.com/realm/realm-cocoa/issues/875
- limit/offset句が利用できない。そのため、RLMResultsで複数件取得した後に、必要な分だけ取得する等は自前で実装する必要あり(結構つらい)Issueにはあがっている。https://github.com/realm/realm-cocoa/issues/947
- 削除時のカスケードができない。関連するオブジェクトを削除する場合は、愚直に処理する必要がある。Issueはこちら。https://github.com/realm/realm-cocoa/issues/1186
- プロパティにnullが指定できない。
パフォーマンス (vs CoreData)
- XCode6 Beta3 / Realm(0.90.5) / iPhone6
- 上で利用しているUserオブジェクトを対象
保存
-指定件数分保存する場合のパフォーマンス
ストレージ \ 件数 | 1,000件 | 10,000件 | 100,000件 |
---|---|---|---|
CoreData | 0.1281(s) | 1.7375(s) | 102.7429 (s) |
Realm | 0.2109(s) | 2.1841(s) | 23.3072(s) |
- データ件数が少ない場合は、以外とCoreDataの速度のほうが早い結果となった。オブジェクトのサイズが大きくなったり、リレーションが増えるとまた違うんだろうけど。。
参照
- Primaryキーで1件フェッチする速度(CoreDataの場合は、Primaryキーに相当するid ※not objectId)
ストレージ \ 件数 | 1,000件 | 10,000件 | 100,000件 |
---|---|---|---|
CoreData | 0.0002(s) | 0.0013(s) | 0.0107 (s) |
Realm | 0.0001(s) | 0.0001(s) | 0.0005(s) |
- Primaryキー以外のパラメータでフェッチする速度(Indexは定義してある前提)
ストレージ \ 件数 | 1,000件 | 10,000件 | 100,000件 |
---|---|---|---|
Realm | 0.0006(s) | 0.0009(s) | 0.0111(s) |
CoreDataからの移行を検討されている方へ
- CoreData最大の使いにくい部分として、NSManagedObjectがスレッドセーフでないことがあげられるが、Realmもスレッド間のオブジェクトの移行はできないため、スレッド間のオブジェクトの取り回しだけが理由であれば移行はあまりおすすめできない。
- ただし、スレッドに関しては、以下の点において、RealmはCoreDataよりも魅力的。
- 別スレッドでデータを保存した際に、明示的に他スレッドに通知する必要がない。
- プライマリキーがアトミックに動作するため、スレッドを意識せずとも確実に一意性を担保できる。(CoreDataの場合は、一意性を担保するにはオブジェクトをシリアルに保存するのが必須のため、結構厄介。参考:CoreDataでatomicにsaveする)
- スレッドをまたいだ場合に、例外で教えてくれる。
- オブジェクトを作成する場合には、RLMObjectのサブクラスを作るだけでテーブル定義もしてくれるのは楽。かつリレーションも楽にはれるのは便利。
- すでに永続化されたオブジェクトのプロパティを操作する際に、トランザクションを明示的に指定する必要があるため、NSManagedObjectのようにいつでもプロパティを簡単に変更できないのはちょっと使いにくい。(局所的に更新処理を書く必要がある)
- マイグレーションに関しては、Realmのほうがバージョン番号をインクリメントするだけなので手間は少ない。ただCoreDataも自動マイグレーションであればマッピングファイルつくるだけなのでそれほど優位性はかわらない。(かな?)
- 削除時のカスケードがRealmはまだできない。関連するオブジェクトを削除する場合は、愚直に処理する必要がある。Issueはこちら。https://github.com/realm/realm-cocoa/issues/1186
- やっぱりパフォーマンスが一番大きいかな。Realmは体感できるぐらい明らかに早い。