はじめに
祝Realm 1.0ヽ(´▽`)/
『realm swift』といった感じのキーワードで検索してRealm Swiftの事を調べようと思うとQiitaのページが上のほうに出てくるのですが、どれも微妙に情報が古いようなので勝手にまとめ直してみました。
あくまで個人的に調べた結果なので、詳細な内容は公式サイトをご確認ください。
ちなみに、Realmの日本語っぽい読み方は『レルム』が一番近いそうですよ。
情報を確認した環境
環境 | 情報 |
---|---|
Xcode | 7.3.1 (7D1014) |
iOS | 9.3 |
Swift | 2.2 |
Realm | 1.0 |
Date | 2016/5/27 |
情報元
Realm is a mobile database: a replacement for SQLite & Core Data
https://realm.io/jp/
Swift Docs - Realm is a mobile database: a replacement for SQLite & Core Data
https://realm.io/jp/docs/swift/latest/
RealmSwift Reference
https://realm.io/docs/swift/latest/api/
realm/realm-cocoa: Realm is a mobile database: a replacement for Core Data & SQLite
https://github.com/realm/realm-cocoa
Realm for Swift まとめ完全版 - Qiita
http://qiita.com/okitsutakatomo/items/9134c5fa8bd4384a2acf
Realm Swiftとは?
公式サイトから引用します。
Realm Swiftはアプリケーションのモデル層を効率的に安全で迅速な方法で記述することができます。
個人的な感想で言うと、速くて使いやすい現代的な(?)データベースです。いつになったら、バージョン1.0になるのか気になります。
インストール
CocoaPodsを利用してインストール
Podfileに『use_frameworks!』と『pod 'RealmSwift'』を追加します。Podfileはこんな感じになります。
# Uncomment this line to define a global platform for your project
platform :ios, '9.0'
# Uncomment this line if you're using Swift
use_frameworks!
pod 'RealmSwift'
あとは、普通にpod installするだけです。実際にRealm Swiftを使うソースでは『import RealmSwift』でモジュールを使えるようにします。
その他のインストール方法
Realm Swiftのドキュメントのページ にFrameworkを自力で追加する方法や、Carthageを使ってインストールする方法の説明があります。
データベースの作成
特に何も指定しなければ、デフォルトのファイルで初期化されます。
// デフォルトのファイルを利用する初期化
let realm = try! Realm()
ファイルを指定して初期化する事も出来ます。
// ファイルを指定して初期化
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
let path = paths[0] + "/test.realm"
let url = NSURL(fileURLWithPath: path)
let realm = try! Realm(fileURL: url)
インメモリで使うように初期化する事も出来ます。
// インメモリで初期化
let config = Realm.Configuration(inMemoryIdentifier: "inMemory")
let realm = try! Realm(configuration: config)
モデルクラス
モデルクラスの作成
モデルクラスはRealm Swiftに用意されているObjectのサブクラスとして作ります。
/// Realmで使える基本的なデータ型
class TestData: Object {
dynamic var flag = true
dynamic var byte: Int8 = 0
dynamic var short: Int16 = 0
dynamic var long: Int32 = 0
dynamic var longlong: Int64 = 0
dynamic var double: Double = 0
dynamic var float: Float = 0
dynamic var string: String = ""
dynamic var date = NSDate()
dynamic var data = NSData()
}
Realmで使える基本的なデータ型の説明は Realm Swiftのドキュメントの早見表 がおすすめです。
プロパティを定義するときの注意点はこんな感じです。
- 基本的に、プロパティは全て『dynamic var』にする必要がある
- List型はletで非オプショナルにする必要がある
- 読み出し専用のプロパティは保存されない
モデルクラスを扱うときの注意点はこんな感じ。
- インスタンスを他のスレッドに渡すことは出来ない
- モデルクラスはinit()が使える必要がある
つまり、オプショナル型以外はデフォルト値が必要。 - モデルクラスを変更したときはマイグレーションが必要
詳しくは後述します。 - CGFloatは使用しない
※以前のRealmはNSDateのミリ秒以下が切り捨てられていましたが、現在は切り捨てられないようになっています
あと、大きな注意点として 『モデルクラスの継承は普通に使えない』 というものがあります。詳しくは Realm Swiftのドキュメント を参照してください。
オプショナル型の使用
プロパティにオプショナル型を使うときの注意点はこんな感じです。
- String、NSDate、NSData型のプロパティはオプショナルに出来ます
- その他の数値型はRealmOptionalでラップすればオプショナルに出来ます
- Object型のプロパティは 必ずオプショナルにする必要があります
プライマリキーの追加
プライマリキーの指定は、primaryKey()をoverrideして、キーのプロパティを返す事で行います。
/// プライマリキーのあるデータ
class PrimaryData: Object {
dynamic var id = 0
dynamic var name = ""
// idをプライマリキーに設定
override static func primaryKey() -> String? {
return "id"
}
}
ちなみに、オブジェクトをRealmに追加/更新すると、その後はプライマリキーを変更する事は出来なくなります。問題になる事は無いと思いますが。
インデックスの追加
インデックスの指定にはindexedProperties()をoverrideします。
/// インデックスを使用するデータ
class IndexedData: Object {
dynamic var price = 0
dynamic var title = ""
// titleにインデックスを貼る
override static func indexedProperties() -> [String] {
return ["title"]
}
}
インデックスが指定出来るのは整数型、Bool、String、NSDateのプロパティです。
保存しないプロパティの指定
保存しないプロパティを指定する場合、ignoredProperties()をoverrideします。
/// 保存しないプロパティのあるデータ
class IgnoredData: Object {
dynamic var firstName = ""
dynamic var lastName = ""
dynamic var tmpID = 0
// tmpIDは保存しない
override static func ignoredProperties() -> [String] {
return ["tmpID"]
}
}
リレーションシップ
普通のリレーションシップ
モデルクラスのプロパティに、モデルクラスを入れる事が出来ます。
/// 飼い犬クラス
class Dog: Object {
dynamic var name = ""
dynamic var owner: Person? // 飼い主は1人だよ
}
/// 飼い主クラス
class Person: Object {
dynamic var name = ""
let dogs = List<Dog>() // 1人で何匹も飼ってるよ
}
普通にプロパティを作れば1対1に、Listを使えば1対多のリレーションシップになります。あとは普通に使えます。
親オブジェクトを取得した場合、そのプロパティになっている別のオブジェクトも自動で取得されます・・・ 言葉で説明するとわかりにくいですが、普通に使えると思っておけば間違いないでしょう。たぶん。
ListはArrayと似たObject専用のコンテナです。いろいろ、便利な機能があります。
逆引き
LinkingObjectsを使うと、自分自身を指してるオブジェクトの一覧を取得する事が出来ます。
/// 飼い犬クラス
class PetDog: Object {
dynamic var name = ""
dynamic var owner: StrayPerson? // 飼い主は1人だよ
}
/// 飼い主クラス ※飼い犬を逆引きするバージョン
class StrayPerson: Object {
dynamic var name = ""
// 自分が飼い主になってる犬を逆引きするよ
let dogs = LinkingObjects(fromType: PetDog.self, property: "owner")
}
オブジェクト操作【基本編】
オブジェクトの追加
単純なオブジェクトの追加であればRealmクラスのインスタンスのwrite()メソッドにブロックを渡して、その中でadd()を使って追加するのがおすすめです。
// Realmのインスタンスを取得
let realm = try! Realm()
// 追加するデータを用意
let dog = Dog()
// データを追加
try! realm.write() {
realm.add(dog)
}
例外処理は、もう少し真面目にやった方が良いかも知れません・・・
オブジェクトの取得
保存されているオブジェクトの型をキーとして、オブジェクトを取得する事が出来ます。
// Realmのインスタンスを取得
let realm = try! Realm()
// Realmに保存されてるDog型のオブジェクトを全て取得
let dogs = realm.objects(Dog)
// ためしに名前を表示
for dog in dogs {
print("name: \(dog.name)")
}
オブジェクトの更新
write()に渡したブロックの中でプロパティを変更する事で、データを更新する事が出来ます。ブロックの外で変更すると例外が発生して怒られます。
// Realmのインスタンスを取得
let realm = try! Realm()
// Realmに保存されてるDog型のオブジェクトを全て取得
let dogs = realm.objects(Dog)
// 先頭の犬を取り出し
if let dog = dogs.first {
// データを更新
try! realm.write() {
dog.name = "First"
}
// write()に渡すブロックの外だと例外発生
// dog.name = "First"
}
オブジェクトの削除
write()に渡したブロックの中でdelete()を使う事でデータを削除出来ます。
// Realmのインスタンスを取得
let realm = try! Realm()
// Realmに保存されてるDog型のオブジェクトを全て取得
let dogs = realm.objects(Dog)
// 一番後ろの犬を取り出し
if let dog = dogs.last {
// さようなら・・・
try! realm.write() {
realm.delete(dog)
}
}
deleteAll()を使うとRealmに保存されている全てのオブジェクトが削除されます。気をつけましょう。
ちなみに、データを削除してもRealmで使用しているファイルのサイズは変化しません。
オブジェクト操作【応用編】
プライマリキーの指定があるオブジェクトの追加&更新
プライマリキーの指定があるモデルクラスの場合、プライマリキーが同じデータを追加しようとすると例外が発生します。
こんなモデルクラスを用意して、
/// プライマリキーのあるデータ
class PrimaryData: Object {
dynamic var id = 0
dynamic var name = ""
// idをプライマリキーに設定
override static func primaryKey() -> String? {
return "id"
}
}
こんな感じにデータを追加しようとします。
// Realmのインスタンスを取得
let realm = try! Realm()
// 追加するデータを用意
let data1 = PrimaryData()
data1.id = 1
data1.name = "First"
// データを追加
try! realm.write() {
realm.add(data1) // 既にidが1のデータが存在すると、例外発生
}
idが1のデータが無ければ良いんですけど、あると例外が発生します。こんな場合のために『データが無ければ追加、あったら上書き』してくれる機能が用意してくれてます。
// Realmのインスタンスを取得
let realm = try! Realm()
// 追加するデータを用意
let data1 = PrimaryData()
data1.id = 1
data1.name = "First"
// データを追加
try! realm.write() {
realm.add(data1, update: true) // データが無ければ追加、あったら上書き
}
便利ですね。
モデルクラスとプライマリキーを指定してオブジェクトを取得
プライマリキーが指定してあれば、こんな感じにオブジェクトを取得する事が出来ます。
// Realmのインスタンスを取得
let realm = try! Realm()
// プライマリキーを指定してオブジェクトを取得
if let data = realm.objectForPrimaryKey(PrimaryData.self, key: 1) {
print("id:1 \(data.name)")
}
オブジェクトを並べ替えて取得
オブジェクトを並び替えて取得する場合はこんな感じになります。
// Realmのインスタンスを取得
let realm = try! Realm()
// 名前で並び替えてPrimaryDataのオブジェクトを取得
let result = realm.objects(PrimaryData).sorted("name", ascending: true)
// ためしにIDと名前を表示
for data in result {
print("\(data.id): \(data.name)")
}
オブジェクトを検索
オブジェクトを検索する場合はこんな感じになります。
// Realmのインスタンスを取得
let realm = try! Realm()
// 名前が『f』ではじまるオブジェクト(大文字小文字の違いは無視)だけを取得
let result = realm.objects(PrimaryData).filter("name BEGINSWITH[c] %@", "f")
// ためしにIDと名前を表示
for data in result {
print("\(data.id): \(data.name)")
}
filter()を上手く使えばいろいろ出来ます。詳しくは 公式サイトのドキュメント をどうぞ。
メソッドチェーン
オブジェクトの検索とソートをつなげて使う事も出来ます。
// Realmのインスタンスを取得
let realm = try! Realm()
// 名前が『f』ではじまるオブジェクト(大文字小文字の違いは無視)を名前で並び替えて取得
let result = realm.objects(PrimaryData).filter("name BEGINSWITH[c] %@", "f").sorted("name", ascending: true)
// ためしにIDと名前を表示
for data in result {
print("\(data.id): \(data.name)")
}
つまり、objects()もfilter()もsorted()もResultsを返すので、そのままつなげて使える、と。そういう事ですね。
オブジェクトの自動更新(ライブアップデート)
RealmのResultsは、情報を取得しに行った瞬間の最新の情報を返します。
別の言い方をすると、write()が終わった時点で(厳密に言うとトランザクションがコミットされた時点で)、オブジェクトの追加や更新が全てのRealmに伝わると言う事です。・・・この認識であってるのか不安ですが。
簡単な例を書いてみました。
// Realmのインスタンスを取得
let realm = try! Realm()
// 全てのデータを削除
try! realm.write() {
realm.deleteAll()
}
// 名前で並び替えてPrimaryDataのオブジェクトを取得
let result = realm.objects(PrimaryData).sorted("name", ascending: true)
// この時点でデータは無いので出力は0
print("result.count: \(result.count)")
// データを追加
try! realm.write() {
let data1 = PrimaryData()
data1.id = 1
data1.name = "First"
realm.add(data1, update: true)
}
// 常に最新の情報を返すので、ここでは1が帰ってくる。便利
print("result.count: \(result.count)")
Resultsは最新の情報を返すから、わざわざフィルターやソートをやり直さなくても良いですよ、って事ですね。他のスレッドで更新された場合も、同じように動作します。動作するはずです。
リレーションシップのあるオブジェクトの削除
1対1や1対多のリレーションシップは、元のオブジェクトを保存する時点でプロパティに入ってるオブジェクトも自動で保存してくれます。取得するときも自動で取得してくれます。ですが・・・ 削除するときは自動で削除してくれません。
簡単な例を書いてみました。こんなクラスを用意して・・・
/// ログをまとめたクラス
class LogData: Object {
dynamic var id = 0
let logs = List<ErrorData>()
override static func primaryKey() -> String? {
return "id"
}
}
/// エラーデータ
class ErrorData: Object {
dynamic var info = ""
dynamic var date = NSDate()
}
こんな感じに使ってみます。
// Realmのインスタンスを取得
let realm = try! Realm()
try! realm.write() {
// 全てのデータを削除
realm.deleteAll()
// データを生成
let log = LogData()
log.id = 0
let error1 = ErrorData()
error1.info = "NullPointerException"
log.logs.append(error1)
let error2 = ErrorData()
error2.info = "ArithmeticException"
log.logs.append(error2)
// LogDataを保存すると、自動的にErrorDataも保存される
realm.add(log)
}
// データの一覧を取得
let logResults = realm.objects(LogData)
let errorResults = realm.objects(ErrorData)
// この時点でLogが1件、Errorが2件
print("logResults.count: \(logResults.count)")
print("errorResults.count: \(errorResults.count)")
// 先頭のLogを削除
if let log = logResults.first {
try! realm.write() {
realm.delete(log)
}
}
// Logは0件になるけど、Errorは2件のまま・・・
print("logResults.count: \(logResults.count)")
print("errorResults.count: \(errorResults.count)")
ちょっと不便ですね。とはいえ不要になったデータを放置するのもあれですから、真面目に削除するようにしましょう。
// 先頭のLogを削除
if let log = logResults.first {
try! realm.write() {
// 先に使ってるデータを削除
realm.delete(log.logs)
// 元のデータを削除
realm.delete(log)
}
}
// LogもErrorも0件になる
print("logResults.count: \(logResults.count)")
print("errorResults.count: \(errorResults.count)")
これぐらいなら簡単ですね。ErrorDataの中でさらに別のオブジェクトを使ってたりすると、ちょっと面倒だったりしますが・・・
マイグレーション
単純なプロパティの追加・削除のマイグレーション
Realmで使用しているデータ構造が変化した場合、Realmにその事を教えてあげる必要があります。単純なプロパティの追加・削除であれば、こんな感じになります。
// スキーマバージョンを上げる。デフォルトのスキーマバージョンは0
let config = Realm.Configuration(schemaVersion: 1)
Realm.Configuration.defaultConfiguration = config
// Realmのインスタンスを取得
let realm = try! Realm()
単純に、スキーマバージョンを上げるだけです。注意点として、スキーマバージョンを上げる処理は、Realmのインスタンスを生成するより前にやっておく必要があります。
個人的には『try! Realm()』の処理は各スレッドで1回だけ行うようにして、Realmのインスタンスを使い回すのがおすすめです。これなら、マイグレーションの処理を入れるのも楽です。
そういう意味では、このまとめは出来が悪いですね・・・
データへの処理が必要となるマイグレーション
単純なプロパティの追加・削除で終わらない場合、プログラムでデータを修正する必要が出てきます。具体的にはこんな感じになります。
// スキーマバージョンを上げる&処理用のブロックを渡す
let config = Realm.Configuration(schemaVersion: 2, migrationBlock: { migration, oldSchemaVersion in
// 処理が必要なスキーマバージョンか?
if oldSchemaVersion == 0 {
// 自力でマイグレーション
migration.enumerate(LogData.className()) { oldObject, newObject in
// 古いデータを元に新しいデータを修正する
}
}
})
Realm.Configuration.defaultConfiguration = config
// Realmのインスタンスを取得
let realm = try! Realm()
oldObjectとnewObjectを使っていろいろやるんですが・・・ わかりにくいですね。Swiftらしくもないし。
マイグレーションで処理が必要になるぐらいなら単純なデータの追加で済ませておいて、プロパティへのアクセスでどうにかした方がマシかもしれません。
通知
RealmやResultsやListに対して、オブジェクトが更新されたときに通知するようにする事が出来ます。
// Realmのインスタンスを取得
let realm = try! Realm()
// LogDataの一覧を指すResultsに通知を設定
// tokenはNotificationToken型のプロパティ
token = realm.objects(LogData).addNotificationBlock() { (changes: RealmCollectionChange) in
switch changes {
case .Initial(let logs): // 最初に1回だけ呼ばれる?
print("Initial logs.count: \(logs.count)")
break
case .Update(let logs, _, _, _): // 更新された
print("Update logs.count: \(logs.count)")
break
case .Error: // エラーが発生した
break
}
}
通知を管理するためのトークンは、こんな感じで用意しておいたプロパティに入れます。
var token: NotificationToken?
その他
2016年5月25日、Realm 1.0が公開になりました。
思ってたより大きな変更が無くてほっとしてます・・・ 公開直前の、週に2回アップデートされてた時期はどうしようかと思いましたが。
最近は公式サイトのドキュメントがすっかり充実してますし、直接そっちを見てもらった方が良いような気もしますが。
Swift Docs - Realm is a mobile database: a replacement for SQLite & Core Data
https://realm.io/jp/docs/swift/latest/