古いバージョンのドキュメントを表示しています。
最新版のドキュメントはこちらからご覧になれます。
Objective‑CのプロジェクトまたはObjective‑CとSwiftが混在するプロジェクトでRealmを利用する場合は、Realm Objective‑Cをご使用ください。 Realm Objective‑CとRealm Swiftを併用することはできません。
Realm Swiftはアプリケーションのモデル層を効率的に安全で迅速な方法で記述することができます。 下記の例をご覧ください:
// 通常のSwiftのクラスと同じように定義します
class Dog: Object {
dynamic var name = ""
dynamic var age = 0
}
class Person: Object {
dynamic var name = ""
dynamic var picture: NSData? = nil // プロパティにOptionalが使えます
let dogs = List<Dog>()
}
// 通常のSwiftのオブジェクトと同じように扱えます
let myDog = Dog()
myDog.name = "Rex"
myDog.age = 1
print("name of dog: \(myDog.name)")
// デフォルトRealmを取得します
let realm = try! Realm()
// 2歳未満のDogオブジェクトを検索します
let puppies = realm.objects(Dog).filter("age < 2")
puppies.count // => 0 (この時点では、Dogオブジェクトはまだ1件も保存されていません)
// データを永続化するのはとても簡単です
try! realm.write {
realm.add(myDog)
}
// クエリの実行結果は自動的に最新の状態に更新されます
puppies.count // => 1
// バックグラウンドスレッドで検索を実行し、値を更新します
DispatchQueue(label: "background").async {
let realm = try! Realm()
let theDog = realm.objects(Dog).filter("age == 1").first
try! realm.write {
theDog!.age = 3
}
}
Core Dataからの移行を検討している場合は、こちらの記事(英文)が参考になります。RealmからCore Dataへの移行に必要なステップが記載されています。
はじめに
Realm Swiftをダウンロード ソースコードはGitHubにあります。
必要条件
- iOS 8以降、またはmacOS 10.9以降、およびすべてのバージョンのtvOSとwatchOS。
- Xcode 7.3以降が必要です。
インストール
- こちらから最新版のRealmをダウンロードしてzipファイルを展開してください。
- Xcodeでプロジェクトをクリックしプロジェクト設定の“General”タブを表示します。展開したフォルダの中にある
ios/swift-2.2/
、watchos/
、tvos/
、またはosx/swift-2.2/
フォルダからRealmSwift.framework
とRealm.framework
を“Embedded Frameworks”にドラッグ&ドロップしてください。このとき、Copy items if neededにチェックが入ってることを確認してからFinishをクリックしてください(1つのプロジェクトで複数のプラットフォームに対応する場合を除く)。 - ユニットテストのターゲットを選択して“Build Settings”タブの“Framework Search Paths”に
RealmSwift.framework
の親フォルダのパスを追加してください。 -
iOS、watchOSまたはtvOSのプロジェクトで利用する場合は、アプリケーションのターゲットの“Build Phases”タブで新しく“Run Script Phase”を追加し、以下のスクリプトをそのままコピー&ペーストしてください。
bash "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework/strip-frameworks.sh"
この手順はアプリケーションを申請する際のiTunes Connectの不具合を回避するために必要です。
- CocoaPods 0.39.0以降をインストールしてください。
- 最新のRealmをインストールするためにコマンドラインから
pod repo update
を実行して、CocoaPodsのSpecリポジトリを更新してください。 - Podfileに
use_frameworks!
と追加します。そしてアプリケーションとテストのターゲットに対してpod 'RealmSwift'
と追加してください。 -
Xcode 8を利用している場合、下記のコードをPodfileに追加してください。Swiftバージョンは自分の環境に合わせて適切に変更してください。
post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '2.3' # or '3.0' end end end
- コマンドラインで
pod install
を実行してください。 - CocoaPodsによって作られた
.xcworkspace
ファイルを開いてください。
- Carthage 0.17.0以降をインストールしてください。
- Cartfileに
github "realm/realm-cocoa"
と追加してください。 -
コマンドラインで
carthage update
を実行してください。ビルドに利用するSwiftツールチェインを変更するには、--toolchain
オプションを指定します。Carthageがビルド済みバイナリをダウンロードしないように、--no-use-binaries
オプションも指定する必要があります。 例:carthage update --toolchain com.apple.dt.toolchain.Swift_2_3 --no-use-binaries
RealmSwift.framework
とRealm.framework
をCarthage/Build/
ディレクトリ内の各プラットフォームのディレクトリから適切なものを選択し、Xcodeの“General”タブにある“Linked Frameworks and Libraries”にドラッグしてください。-
iOS/tvOS/watchOS: アプリケーションのターゲットの“Build Phases”の“+”ボタンをクリックし、以下のような“New Run Script Phase”を追加します。
/usr/local/bin/carthage copy-frameworks
このとき、“Input Files”にはフレームワークのパスを指定します。
例:
$(SRCROOT)/Carthage/Build/iOS/Realm.framework
$(SRCROOT)/Carthage/Build/iOS/RealmSwift.frameworkこの手順はアプリケーションを申請する際のiTunes Connectの不具合を回避するために必要です。
Realmフレームワークをインポートする
Realmをインポートし、コードから利用できるようにするために、Realmを使いたい各ソースファイルの先頭にimport RealmSwift
と記述してください。これで使い始めるための準備はすべて完了です!
tvOS
tvOSではDocumentsディレクトリへの書き込みは禁止されているので、tvOS用のフレームワークでは、デフォルトの保存先はNSCachesDirectory
に変わっています。ただし、キャッシュディレクトリ内のファイルは常にシステムによって削除される可能性があるので注意してください。そのため、tvOSにおけるRealmの利用は、復元可能なデータのキャッシュなどに利用し、消えては困る重要な(再生成できない)ユーザーデータなどを保存するのは避けてください。
エクステンション(例: Top Shelf)とtvOSアプリの間でデータを共有するためにApp Groupコンテナを利用する場合は、下記のようにコンテナURL内の/Library/Caches/
ディレクトリを使用してください。それ以外のディレクトリではtvOSの制限により、Realmに必要なファイルを作成することができません。
let fileURL = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.io.realm.examples.extension")!
.appendingPathComponent("Library/Caches/default.realm")
別の利用方法として、Realmファイルをコンテンツデータとしてアプリケーションにバンドルする使い方も便利です。tvOSアプリケーションのサイズはApp Storeガイドラインにて200MB以下と規定されているのでバンドルするデータの容量に注意してください。
上記のキャッシュとしての使い方と、データをアプリケーションにバンドルする使い方について、両方のサンプルコードを提供しています。利用する際の参考にしてください。
Realm Browser
RealmBrowserは、.realm
データベースを閲覧、編集するMac アプリケーションです。
また、Tools > Generate demo database と選択すると、サンプルデータを含むテスト用のRealmデータベースを作ることができます。
開発中のアプリのRealmファイルがどの場所に格納されているかは、このStackOverflowの回答が参考になります。
Realm BrowserはMac App Store からダウンロードできます。
Xcode Plugin
Xcodeプラグインを使うことでRealmモデルファイルの作成が簡単になります。
このXcodeプラグインをインストールする一番簡単な方法は、Alcatrazで“RealmPlugin”と検索することです。
また、手作業でインストールする場合は、最新版のRealmのplugin/RealmPlugin.xcodeproj
をビルドすることでインストールができます。
プラグインをインストール後にXcodeを再起動すると、Xcodeで新しくファイルを作成時(File > New > File...
または⌘N
)にRealm Modelという選択肢が追加されています。
APIリファレンス
Realmで使用できるすべてのクラスとメソッドに関しては、APIリファレンスをご覧ください。
サンプルコード
最新版のRealmのexamples
フォルダにiOSとOS Xそれぞれのサンプルコードがあります。 マイグレーション、UITableViewController
と組み合わせた使い方、暗号化やコマンドラインツールなど、さまざまな機能が紹介されています。ぜひ、ご参考にしてください。
ヘルプ
- 使い方に困ったときは、StackOverflowで#realmタグを付けて質問してください。私たちは毎日StackOverflowをチェックしています。
- さらに複雑な問題に対する質問は、こちらの Slackチャットにて聞いてください。(質問は日本語で構いません)
- バグ報告や機能リクエストについては GitHubのIssuesにご報告ください。
- Community Newsletterに参加することで定期的にRealmに関するTipsやユースケース、ブログの更新やチュートリアルなど、Realm の最新情報が届きます。
モデル
Realmのデータモデルは、普通のSwiftのクラスとして定義できます。単にObject
または既存のモデルクラスのサブクラスを作ることでアプリケーションで使うデータモデルを作成できます。
Realmのモデルオブジェクトは、他のオブジェクトとほとんど同様に機能するので、一般的なクラスと同様にメソッドやプロトコルを追加することができます。
主な制限としては、Realmモデルクラスのインスタンスは他のスレッドに渡すことができません。またプロパティのインスタンス変数に直接アクセスすることはできません。
リレーションシップとネストしたデータ構造は、対象の型のプロパティを持たせるかList
を利用します。
import RealmSwift
// Dog model
class Dog: Object {
dynamic var name = ""
dynamic var owner: Person? // Properties can be optional
}
// Person model
class Person: Object {
dynamic var name = ""
dynamic var birthdate = NSDate(timeIntervalSince1970: 1)
let dogs = List<Dog>()
}
Realmはアプリケーションの起動時に、定義されたすべてのモデルクラスを解析しています。まったく使われていないクラスも含めて、すべてのモデルクラスは正しく定義されていなければなりません。
RealmSwiftではモデルの情報を取得するためにSwift.reflect(_:)
関数を利用しています。 そのため、モデルに対してイニシャライザinit()
の呼び出しが必ず成功する必要があります。 非Optionalなプロパティにおいて初期値を必ず指定しなければならないのはこの理由によります。
詳しくはObjectをご覧ください。
対応しているデータ型
Realmでは、次に示すデータ型をサポートしています: Bool
、Int8
、Int16
、Int32
、Int64
、Double
、Float
、String
、NSDate
、およびNSData
。
CGFloat
型はプラットフォーム(CPUアーキテクチャ)によって実際の定義が変わるため、使用しないようにしてください。
String
、NSDate
およびNSData
型のプロパティはOptionalとして指定できます。Object
型のプロパティは必ずOptionalでなければなりません。数値型をOptionalとして扱うにはRealmOptionalで実際の値をラップします。
リレーションシップ(関連)
Object
はプロパティにObject
やObject
を使用して、他のオブジェクトとの関連を定義することができます。
List
はArray
とよく似たAPIを持ち、添え字を使って各要素にアクセスすることもできます。
Array
と異なる点は、List
はObject
のサブクラスのみ格納することができるということです。
詳しくはList
をご覧ください。
すでにこれまでのステップでPersonモデルを定義しました。もう一つDogモデルを作成し関連を定義してみましょう。
class Dog: Object {
dynamic var name = ""
}
1対1のリレーションシップ
モデル間で1対1や1対多の関連を持たせるには、別のObject
モデルクラスを持つプロパティを下記のように定義します:
class Dog: Object {
// ... other property declarations
dynamic var owner: Person? // to-one relationships must be optional
}
このプロパティは以下のように使えます:
let jim = Person()
let rex = Dog()
rex.owner = jim
Object
をプロパティとして持つとき、ネストされたプロパティには通常のオブジェクトのプロパティと同じ文法でアクセスできます。上記の例では、rex.owner?.address.country
とすると、Realmはネストしたオブジェクトを必要に応じて自動的にフェッチします。
1対多のリレーションシップ
1対多の関連を定義するにはObject
を持つプロパティを定義します。Object
は別のObject
を含み、ミュータブルなArray
とよく似たAPIを持ちます。
PersonクラスがDogクラスを1対多の関連として持つようにするには、次のようにList<Dog>
のプロパティを定義します:
class Person: Object {
// ... other property declarations
let dogs = List<Dog>()
}
これで、下記のようにしてObject
型のプロパティに対してアクセスしたり、オブジェクトを追加したりできるようになりました:
let someDogs = realm.objects(Dog).filter("name contains 'Fido'")
jim.dogs.append(objectsIn: someDogs)
jim.dogs.append(rex)
逆方向の関連
Realmの関連は一方通行です。Dog
への1対多の関連であるPerson.dogs
のプロパティと、Person
への1対1の関連であるDog.owner
の2つのプロパティは、それぞれ互いに独立して動作します。Person.dogs
プロパティにDog
オブジェクトを追加しても、Dog.owner
プロパティにPerson
のオブジェクトは自動的に追加されたりはしません。手作業でこの2つの関連の整合性を保とうとすることは、間違いを起こしやすく、複雑で冗長さを招くので、Realmでは下記に示すような、逆方向の関連(バックリンクとも呼ばれます)を表現するためのプロパティを使用することができます。
Linkng Object型のプロパティを使用することで、関連元のオブジェクトを特定のプロパティを使って取得することができます。例えば、Dog
クラスにowners
という自分自身を関連として保持しているPerson
オブジェクトをすべて取得するというプロパティを持たせることができます。その方法はowners
プロパティをLinkingObjects
型として定義し、Person
クラスの関連であると指定するだけです。
class Dog: Object {
dynamic var name = ""
dynamic var age = 0
let owners = LinkingObjects(fromType: Person.self, property: "dogs")
}
Optional型のプロパティ
String
、NSDate
およびNSData
型のプロパティは標準のSwiftの文法を用いて、Optional型あるいは通常の型として定義することが可能です。 数値型をOptinalとして扱うにはRealmOptionalでラップする必要があります。
class Person: Object {
// Optional string property, defaulting to nil
dynamic var name: String? = nil
// Optional int property, defaulting to nil
// RealmOptional properties should always be declared with `let`,
// as assigning to them directly will not work as desired
let age = RealmOptional<Int>()
}
let realm = try! Realm()
try! realm.write() {
var person = realm.create(Person.self, value: ["Jane", 27])
// Reading from or modifying a `RealmOptional` is done via the `value` property
person.age.value = 28
}
RealmOptional
はInt
、Float
、Double
、Bool
とすべてのサイズ固定型のInt
型(Int8
、Int16
、Int32
、Int64
)に対して利用できます。
チートシート(早見表)
以下はプロパティ定義のデータ型ごとの早見表です。
Type | 非Optional | Optional |
---|---|---|
Bool | dynamic var value = false | let value = RealmOptional<Bool>() |
Int | dynamic var value = 0 | let value = RealmOptional<Int>() |
Float | dynamic var value: Float = 0.0 | let value = RealmOptional<Float>() |
Double | dynamic var value: Double = 0.0 | let value = RealmOptional<Double>() |
String | dynamic var value = "" | dynamic var value: String? = nil |
Data | dynamic var value = NSData() | dynamic var value: NSData? = nil |
Date | dynamic var value = NSDate() | dynamic var value: NSDate? = nil |
Object | n/a: 非Optionalにはできません | dynamic var value: Class? |
List | let value = List<Class>() | n/a: Optionalにはできません |
LinkingObjects | let value = LinkingObjects(fromType: Class.self, property: "property") | n/a: Optionalにはできません |
プロパティ属性
Realmモデルのプロパティは専用のアクセスメソッドに置き換えられるため、dynamic var
属性となければなりません。
このルールには一つだけ例外があり、List
プロパティはdynamic
として定義できません。なぜなら動的ディスパッチを利用したときに使われるObjective‑CランタイムではGenericsを表現できないため、dynamic
は使用できません。また常にlet
を用いて宣言する必要があります。
インデックス付きプロパティ
クラスメソッドのObject.indexedProperties()
をオーバーライドすることで、インデックスに追加するプロパティを指定できます:
class Book: Object {
dynamic var price = 0
dynamic var title = ""
override static func indexedProperties() -> [String] {
return ["title"]
}
}
文字列と整数型、Bool型およびNSDate
型のプロパティはインデックスに対応しています。
プロパティをインデックスに登録することは=
やIN
を使ったクエリの速度を大幅に向上します。その代わりにオブジェクトを作成する速度は少し遅くなります。
オブジェクトの自動更新(ライブアップデート)
Object
のインスタンスは常に最新の内部データの状態に自動的に更新されています。つまり、オブジェクトをいちいち再読み込みする必要はありません。プロパティを変更すると、その変更はただちに同じオブジェクトを参照している他のインスタンスに反映されます。
let myDog = Dog()
myDog.name = "Fido"
myDog.age = 1
try! realm.write {
realm.add(myDog)
}
let myPuppy = realm.objects(Dog).filter("age == 1").first
try! realm.write {
myPuppy!.age = 2
}
print("age of my dog: \(myDog.age)") // => 2
このようなObject
の性質は、Realmが高速にかつ効率的に動作するだけでなく、アプリケーションのコードをよりシンプルに、リアクティブにします。例えば、あるRealmオブジェクトのデータを表示しているUIコンポーネントがあるとした場合、そのUIコンポーネントを再描画する前に、いちいち再読み込みしたり、検索し直す必要はありません。
Realmの通知を監視することで、いつRealmのデータが更新されたのかが分かります。データが更新されたタイミングでアプリケーションのUIを更新するべきでしょう。別の方法として、キー値監視(KVO)を用いて、特定のObject
のプロパティの変更を知ることができます。
プライマリキー
クラスメソッドのObject.primaryKey()
をオーバーライドすることで、そのモデルのプライマリキーを指定できます。プライマリキーを使うと、オブジェクトを効率的に検索・更新することができ、一意性を保つこともできます。
class Person: Object {
dynamic var id = 0
dynamic var name = ""
override static func primaryKey() -> String? {
return "id"
}
}
保存しないプロパティ
クラスメソッドのObject.ignoredProperties()
をオーバーライドすることで、Realmに保存しないプロパティを指定することができます。
保存しないプロパティについてはRealmはプロパティの操作に介入しません。つまり普通のプロパティとしてインスタンス変数に値は格納され、getter/setterメソッドをオーバーライドすることも自由にできます。
class Person: Object {
dynamic var tmpID = 0
var name: String { // 読み込み専用(read-only)プロパティは自動的に保存しないプロパティとして扱われます
return "\(firstName) \(lastName)"
}
dynamic var firstName = ""
dynamic var lastName = ""
override static func ignoredProperties() -> [String] {
return ["tmpID"]
}
}
モデルクラスの継承
複数のモデル間で共通のコードを再利用するために、モデルクラスをさらに継承してサブクラスを作ることができます。しかし、一般的なCocoaフレームワークのクラスを継承したときのようなポリモーフィズム(多態性)は機能しません。できることは下記に限られます:
- スーパークラスのクラスメソッド、インスタンスメソッド、およびプロパティはサブクラスに継承されます。
- スーパークラスを引数としてとるメソッドと関数はサブクラスから操作できます。
下記の挙動はサポートされません:
- 継承関係にあるクラス間のキャスト(例: サブクラスからサブクラス、サブクラスからスーパークラス、スーパークラスからサブクラスなど)
- (スーパークラスを指定して)複数のサブクラスを一度に検索する
- 複数の異なるクラスをコンテナオブジェクト(
List
とResults
)に格納する。
このような多態性のサポートは機能のロードマップに予定されています。それまでの間の一時的な対応策としていくつかのサンプルコードを提供しています。
あるいは、(実装の都合が許すなら)サブクラスよりコンポジションを使って共通のロジックを別のクラスに持たせることを推奨します:
// 元になるモデル
class Animal: Object {
dynamic var age = 0
}
// コンポジションとしてAnimalクラスのオブジェクトをDuckに持たせる
class Duck: Object {
dynamic var animal: Animal? = nil
dynamic var name = ""
}
class Frog: Object {
dynamic var animal: Animal? = nil
dynamic var dateProp = NSDate()
}
// 使い方
let duck = Duck(value: [ "animal": [ "age": 3 ], "name": "Gustav" ])
コレクションクラスについて
RealmにはRealmオブジェクトの集合を扱うためのクラスが複数あります。それらを総称して”Realmコレクションクラス”と呼びます。
Results
、クエリを用いて取得したオブジェクトを表します。List
、モデルオブジェクトにて1対多の関係を表すためのクラスです。LinkingObjects
, モデルにおいて逆方向の関連を表すクラスです。RealmCollection
、Realmコレクションクラスに共通のインターフェースを定義したプロトコルです。すべてのRealmコレクションクラスはこのプロトコルに準拠しています。AnyRealmCollection
、Results
やList
など具象型のRealmコレクションクラスに操作を移譲する型消去されたラッパークラスです。
Realmコレクションクラスはそれぞれのクラス間で一貫した振る舞いを提供するために、RealmCollection
プロトコルに準拠しています。このプロトコルはCollectionType
を継承しています。そのため、標準ライブラリのコレクションクラスと同じように扱うことができます。またクエリや並べ替え、集計関数などRealmコレクションクラスで共通のAPIはこのプロトコルで宣言されています。Object
には、プロトコルで定義されたメソッドに加えて、追加や削除などコレクションを変更するためのメソッドが定義されています。
RealmCollection
プロトコルを使うと、Realmコレクションクラスをジェネリックに取り扱うコードが書けます。
func operateOn<C: RealmCollection>(collection: C) {
// ResultsまたはListのどちらでも渡すことができます
print("operating on collection containing \(collection.count) objects")
}
Swiftではassociatedtypeを持つプロトコルは具象型として扱えないため、Realmコレクションクラスをプロパティや変数として保持するには、AnyRealmCollection
のような型消去されたラッパークラスが必要になります。
class ViewController {
// let collection: RealmCollection
// ^
// error: protocol 'RealmCollection' can only be used
// as a generic constraint because it has Self or
// associated type requirements
//
// init<C: RealmCollection>(collection: C) where C.Element == MyModel {
// self.collection = collection
// }
let collection: AnyRealmCollection<MyModel>
init<C: RealmCollection>(collection: C) where C.Element == MyModel {
self.collection = AnyRealmCollection(collection)
}
}
書き込み
Realmへのオブジェクトの追加、変更、削除は、トランザクションの内部で行う必要があります。
Realmモデルクラスは、通常のSwiftオブジェクトと同じようにインスタンス化し、使うことができます。アンマネージド(Unmanaged、まだRealmに追加されてない)Object
のインスタンスは、通常のSwiftのオブジェクトと同様に振る舞います。
永続化されたRealm
、Object
、Results
またはObject
のインスタンスは、生成されたスレッド内でなければ利用することができません。 別のスレッドで利用されると例外が発生します。これはRealmがトランザクションを分離するための必要な仕様です。
永続化された{{ RLMRealm }}
、Object
、{{ RLMResults }}
または{{ RLMArray }}
のインスタンスは、生成されたスレッド内でなければ利用することができません。 別のスレッドで利用されると例外が発生します。これはRealmがトランザクションを分離するための必要な仕様です。
スレッド間でデータを共有したり、アプリケーションの再起動時に以前のデータを利用するには、Realmにデータを保存しなければなりません。これらの操作は、トランザクションの中で行う必要があります。
トランザクションには、無視できないオーバーヘッドが発生しますので、できるだけトランザクションの数は最小限に抑えることが望ましいです。
トランザクションはディスクI/Oを伴う操作などと同様に失敗する可能性があります。 Realm.write()
とRealm.commitWrite()
はどちらもthrows
メソッドとして宣言されており、ディスクの容量不足などで失敗した際のエラーからリカバリすることができます。簡単にするためにこのドキュメントやサンプルコードではエラー処理をしていませんが、実際のアプリケーションでは、エラーを処理して必要に応じてリカバリするべきです。
オブジェクトの生成
Objectのサブクラスとして定義したモデルをインスタンス化して、新しいオブジェクトとしてRealmに保存します。
次のような簡単なモデルを考えます:
class Dog: Object {
dynamic var name = ""
dynamic var age = 0
}
オブジェクトを作成するにはいくつかの方法があります:
// (1) Dogクラスのオブジェクトを作成し、プロパティに値をセットする
var myDog = Dog()
myDog.name = "Rex"
myDog.age = 10
// (2) Dictionaryの値を使ってDogクラスのオブジェクトを作成する
let myOtherDog = Dog(value: ["name" : "Pluto", "age": 3])
// (3) Arrayの値を使ってDogクラスのオブジェクトを作成する
let myThirdDog = Dog(value: ["Fido", 5])
- Objective‑Cにおけるalloc-initや、Swiftにおける指定イニシャライザを使うのはオブジェクトを作るもっともわかりやすい方法です。 すべてのプロパティの値を、オブジェクトがRealmに追加される前にセットする必要があるので注意してください。
- 適切なキーと値の組み合わせを持つディクショナリからオブジェクトを生成することもできます。
- 最後の方法は配列からオブジェクトを作る方法です。配列の各要素は、生成するモデルのプロパティと同じ順序で並んでいる必要があります。
ネストしたオブジェクト
Object
のサブクラス、またはObject
のプロパティを持つモデルオブジェクトは、ネストした配列やディクショナリを使って再帰的にオブジェクトをセットすることができます。
そのとき、配列またはディクショナリを使って、下記のように簡単にそれぞれのオブジェクトを生成することができます:
// すでに存在するオブジェクトを渡す代わりに...
let aPerson = Person(value: ["Jane", 30, [aDog, anotherDog]])
// ...配列を使ってその場で値を渡すことができます
let anotherPerson = Person(value: ["Jane", 30, [["Buster", 5], ["Buddy", 6]]])
この方法は、ネストした配列やディクショナリがどのような組み合わせであっても動作します。この場合のList
はObject
のオブジェクトだけを格納できることに注意してください。String
のような他の基本的な型は格納できません。
オブジェクトの追加
オブジェクトをRealmに追加するには次のようにします:
// Personオブジェクトを作成する
let author = Person()
author.name = "David Foster Wallace"
// デフォルトRealmを取得する
let realm = try! Realm()
// Realmの取得はスレッドごとに1度だけ必要になります
// トランザクションを開始して、オブジェクトをRealmに追加する
try! realm.write {
realm.add(author)
}
オブジェクトをRealmに追加した後も、続けて使用することができます。そして、オブジェクトに対するすべての変更が保存されます。(オブジェクトの変更は、トランザクションの中で行わなければなりません)。別のスレッドで、同じRealmに保存されているオブジェクトに変更があった場合は、トランザクションが完了した時点ですべての変更が適用されます。
同時に発生した書き込み処理は、互いの書き込み処理をブロックします。 これは類似の他のデータベースでも同様で、よく使われるベストプラクティスとして、書き込み処理を別のスレッドに分けることを推奨します。
RealmはMVCCアーキテクチャーを採用しているので、書き込み処理の最中でも読み込み処理をブロックすることはありません。同時に複数のスレッドから書き込みをするのでなければ、大きな単位でトランザクションを使いましょう。細かいトランザクションを使うよりこの特性を活かすことができます。トランザクションがコミットされると、すべての他のRealmインスタンスに通知され、自動的に最新の状態に更新されます。。
オブジェクトの更新
Realmのオブジェクトを更新するにはいくつかの方法があります。それぞれの方法には、状況によって異なるトレードオフがあるので、状況に応じて適切な方法を選択してください:
プロパティへの代入
オブジェクトを更新するには、トランザクションの中でプロパティをセットします。
// トランザクションを開始して、オブジェクトを更新する
try! realm.write {
author.name = "Thomas Pynchon"
}
プライマリキーを使ってオブジェクトを作成・更新する
モデルにプライマリキーを指定しているなら、Realm().add(_:update:)
を使って、オブジェクトがすでに存在する場合は更新、存在しない場合は新しく追加というように、追加または更新を一度に行うことができます。
// 以前に保存したものと同じプライマリキーを持つBookオブジェクトを作成する
let cheeseBook = Book()
cheeseBook.title = "Cheese recipes"
cheeseBook.price = 9000
cheeseBook.id = 1
// id = 1のBookオブジェクトの値を更新する
try! realm.write {
realm.add(cheeseBook, update: true)
}
ここでid = 1
のBookオブジェクトがすでにRealmに保存されていた場合は、引数に渡されたオブジェクトで既存のオブジェクトを更新します。 もしid = 1
のBookオブジェクトがRealmに保存されていない場合は、新しいBookオブジェクトを作成し、Realmに追加されます。
オブジェクトのプロパティを部分的に更新するには、オブジェクトのサブセットを渡します。具体的には下記のように更新したいプロパティの値とプライマリキーだけが含まれたディクショナリを渡します。
// プライマリキーが`1`のBookオブジェクトがすでにあるとき、
try! realm.write {
realm.create(Book.self, value: ["id": 1, "price": 9000.0], update: true)
// タイトルはそのままで値段のプロパティだけを更新することができます。
}
モデルにプライマリキーが定義されてない場合は、update: true
のようにupdate
引数にtrue
を渡すことはできません。
Realmに同じプライマリキーを持つオブジェクトが保存されていない場合は、このメソッドを呼ぶたびに新しいオブジェクトが作られて保存されます。
値を更新しようとするときは nil
の存在に注意してください。 nil
はOptionalなプロパティに対しては正しい値とみなされます。もし、ディクショナリの値としてnil
を渡した場合、オブジェクトやプロパティは空の値に更新されます。データが空にするつもりがないなら、更新したい値だけを渡していることを確認してください。
キー値コーディング
Object
、Result
、List
クラスはいずれも キー値コーディング(KVC)に対応しています。 実行時にアップデートするプロパティが決定する場合に使用すると便利です。
また、キー値コーディングをコレクションオブジェクトに対して使用すると、多数のオブジェクトをループしてインスタンス化することが避けられるので、一括の更新を非常に効率的に行うことができます。
let persons = realm.objects(Person)
try! realm.write {
persons.first?.setValue(true, forKeyPath: "isFirst")
// すべてのPersonオブジェクトのプロパティを"Earth"に更新します
persons.setValue("Earth", forKeyPath: "planet")
}
オブジェクトの削除
オブジェクトを削除するには、トランザクションの中で削除したいオブジェクトをRealm().delete(_:)
メソッドに渡します。
// let cheeseBook = ... 保存されているBookオブジェクトを取得して、
// トランザクションを開始してオブジェクトを削除します
try! realm.write {
realm.delete(cheeseBook)
}
下記のように、Realmに保存されてるオブジェクトをすべて削除することもできます。 ディスクスペースを効率的に再利用するために、Realmファイルのサイズはそのまま維持されることに注意してください。
// Realmに保存されているすべてのオブジェクトを削除します。
try! realm.write {
realm.deleteAll()
}
クエリ
クエリを実行するとObject
を含んだResults
オブジェクトが結果として返ってきます。
Results
は、Array
に非常によく似たようなAPIを持ち、添え字を使ってオブジェクトにアクセスすることもできます。
Array
と異なる点は、Results
は、Object
のサブクラスのみを含むことができるという点です。
Realmにおけるすべてのクエリとプロパティへのアクセスを含むは遅延ロードされます。プロパティにアクセスした時に、初めてデータが読み込まれます。
クエリを実行したときに返ってくる結果は、コピーではありません。トランザクションを使ってそのデータを変更した場合、ディスクのデータを直接変更したことになります。
同様に、Results
に含まれるObject
から、関連のオブジェクトを次々とたどりながら取得することもできます。
クエリの実行は、結果のオブジェクト(Results
)が実際に使用されるまだ遅延されます。つまり、メソッドチェーンなどによって一時的にResults
オブジェクトが作られても、それだけでは(Results
は使用されていないので)その時点ではクエリは実行されません。
(Results
の要素のアクセスするなどして)実際にクエリが実行された後、あるいはNotificationブロックが追加されたときは、Results
はRealmに変更があるたびにバックグラウンドスレッドでクエリを実行し、自動的に最新の状態にアップデートされます。
もっとも基本的なオブジェクトを取得する方法はRealm().objects(_:)
メソッドを使うことです。
このメソッドは、デフォルトRealmに保存されているObjectオブジェクトのうち、指定したクラスのすべてのインスタンスを含むResultsを返します。
let dogs = realm.objects(Dog) // デフォルトRealmから、すべてのDogオブジェクトを取得します
検索条件を指定する
普段からNSPredicate
の扱いに慣れているなら、Realmにおけるオブジェクトの検索方法を知っているも同然です。
Objects
、Realm
、List
、Results
はすべて特定のObject
オブジェクトをNSPredicate
を使って検索するためのメソッドをサポートしています。 NSArray
を検索するときと同じように、直接NSPredicate
オブジェクトを渡す、条件部分だけを文字列で渡す、などの方法が利用できます。
下記の例は、Results().filter(_:...)
メソッドを使って、Dogクラスのcolor = "tan"
かつ、nameの値がB
から始まるという条件に合致するDog クラスのインスタンスをデフォルトRealmから取得します。
// 文字列で検索条件を指定します
var tanDogs = realm.objects(Dog).filter("color = 'tan' AND name BEGINSWITH 'B'")
// NSPredicateを使って検索条件を指定します
let predicate = NSPredicate(format: "color = %@ AND name BEGINSWITH %@", "tan", "B")
tanDogs = realm.objects(Dog).filter(predicate)
詳しくは、AppleのPredicates Programming GuideやRealmが提供しているNSPredicateチートシートをご覧ください。Realmでは多数のNSPredicate構文をサポートしています。
- 比較演算子はプロパティ名と定数に対して使用できます。左辺と右辺のうち少なくとも一方はプロパティ名でなければなりません。
-
比較演算子として==、<=、<、>=、>、!=、BETWEENが
Int
、Int8
、Int16
、Int32
、Int64
、Float
、Double
、NSDate
に対して使用できます。(例)age == 45
-
同一性の比較==、!=
(例)
Results<Employee>().filter("company == %@", company)
- bool型のプロパティに対しては比較演算子として==と!=が使用できます。
-
String
とNSData
型のプロパティに対しては、==、!=、BEGINSWITH、CONTAINS、ENDSWITH演算子が使用できます。(例)name CONTAINS ‘Ja’
- 文字列に対して大文字小文字を無視して比較するには、name CONTAINS[c] ‘Ja’のようにします。大文字小文字として扱われるのはアルファベットの”A-Z”および”a-z”であることに注意してください。
-
論理演算子として“AND”、“OR”、“NOT”が使用できます。
(例)name BEGINSWITH ‘J’ AND age >= 32
-
いずれかの条件と一致するかどうか: IN
(例)name IN {‘Lisa’, ‘Spike’, ‘Hachi’}
-
nilとの比較: ==, !=
(例)
Results<Company>().filter("ceo == nil")
。Realmでは
nil
は「何もない」ことを意味するのではなく、特別な値として扱われます。そのため、一般のSQLと異なりnil
はnil
自身と一致します。 -
いずれかの要素が条件と一致するかどうか: ANY
(例)ANY student.age < 21
-
集計関数
@count
、@min
、@max
、@sum
および@avg
がObject
とResults
のプロパティに対してサポートされています。(例)
realm.objects(Company).filter("employees.@count > 5")
上記の例は
employees
を5件以上持つCompany
オブジェクトを取得します。 - サブクエリは限定的にサポートされていて、下記の制限があります:
- サブクエリに適用できる集計関数は
@count
だけです。 SUBQUERY(…).@count
と比較できるのは定数だけです。- 相関サブクエリはサポートされていません。
- サブクエリに適用できる集計関数は
詳しくは、Results().filter(_:...)
をご覧ください。
並べ替え
Results
は、1つあるいは複数のプロパティの値を使って並べ替えることができます。下記は、先ほどのDogオブジェクトを検索した結果を、nameプロパティのアルファベット順で並べ替える例です。
// color = 'tan'かつ名前が"B"から始まるDogオブジェクトを、名前の昇順で取得します
let sortedDogs = realm.objects(Dog.self).filter("color = 'tan' AND name BEGINSWITH 'B'").sorted(byProperty: "name")
詳しくは、Results().filter(_:...)
and Results().sorted(_:ascending:)
をご覧ください。
クエリの実行結果(Results
)の順序はソートしなければ保証されません。パフォーマンス上の都合により、オブジェクトの挿入順は保持されません。挿入順を保持するいくつかの方法は、こちらに記載しています。
クエリの連鎖
他のデータベースと比較してRealmを使う利点として、非常に小さなオーバーヘッドで、連鎖したクエリを実行できる点が挙げられます。 例えば、color = tan
かつ、name
がB
からはじまるDogオブジェクトを検索したい場合、以下のように連鎖的にメソッドを呼び出すことができます。
let tanDogs = realm.objects(Dog).filter("color = 'tan'")
let tanDogsWithBNames = tanDogs.filter("name BEGINSWITH 'B'")
検索結果({{ RLMResults }})の自動更新(ライブアップデート)
{{ RLMResults }}
は常に最新の状態に自動的に更新されます。このため、同じ検索条件なら繰り返し検索を実行して、結果を取得し直す必要はありません。{{ RLMResults }}
は常に現在のスレッドにおける最新の状態(同じスレッドのトランザクション中の状態を含む)を反映します。唯一の例外は、for...in
ループを使用するときです。for...in
ループの間でオブジェクトが削除されたり、クエリの条件に合致しなくなるような変更がされた場合でも、ループ開始時点のすべてのオブジェクトを列挙します。
let puppies = realm.objects(Dog).filter("age < 2")
puppies.count // => 0
try! realm.write {
realm.create(Dog.self, value: ["name": "Fido", "age": 1])
}
puppies.count // => 1
この仕組みはすべての{{ RLMResults }}
オブジェクトに(検索条件や、並べ替えの有無にかかわらず)適用されます。
{{ RLMResults }}
が持つこの性質によって、Realmは効率的で高速な処理を実現しています。さらにアプリケーションのコードをシンプルかつリアクティブにすることを可能にします。例えば、クエリの検索結果を表示するビューコントローラの場合は、{{ RLMResults }}
を保持して表示に使うことで、アクセスするたびに再検索することなく、常に最新のデータを表示することができます。
合わせてRealmの通知を利用するとRealmのデータに更新がありアプリケーションのUIを更新する必要があるということを知ることができます。その場合も、{{ RLMResults }}
を取得し直す必要はありません。
この自動更新の機能のために、件数やインデックスの値が一定ではないということは重要です。そのため高速列挙(Fast Enumeration)を使って要素をループしている時だけは{{ RLMResults }}
は自動更新されません。このことにより、検索結果をループ中に結果に影響のある変更を加えても、すべてのマッチするオブジェクトを正しく変更することができます。
try! realm.write {
for person in realm.objects(Person).filter("age == 10") {
person.age += 1
}
}
同じことは{{ RLMResults }}
に対してキー値コーディングを利用することでも可能です。
取得データの数を制限
Realm以外のほとんどのデータベースには検索結果を「ページネーション(ページング)」する仕組みが備わっています(例えばSQLiteの’LIMIT’句によるものなどです)。この仕組みはディスクの過剰な読み込み、あるいは一度に大量のデータをメモリに読み込むことを避けるために使われます。
Realmのクエリは遅延実行されるので、このような「ページネーション」の仕組みはまったく必要ありません。なぜなら、Realmはクエリの実行結果の要素に対して、実際にアクセスしたときだけオブジェクトを読み込むからです。
UIや実装の都合により、クエリの実行結果の一部分だけが必要だったとします。そのときは、単に{{ RLMResults }}
オブジェクトを用いて、必要な要素にだけアクセスすれば良いのです。
// データを5件に制限したい場合は、
// 単に最初から5番目までのオブジェクトにアクセスします
let dogs = try! Realm().objects(Dog)
for i in 0..<5 {
let dog = dogs[i]
// ...
}
Realmについて
デフォルトRealm
すでにお気づきだと思いますが、これまでRealm()
メソッドを呼ぶことで、変数realm
を初期化してきました。
このメソッドは、各アプリケーションのDocumentsフォルダに作られた”default.realm”ファイルのRealm
オブジェクトを返します。
Realm Configuration
Realmファイルの保存場所などの、Realmに対する設定をカスタマイズするには、Realm.Configuration
オブジェクトを使います。
設定オブジェクトはRealmのインスタンスを取得する際にRealm(configuration: config)
メソッドに渡すこともできますし、Realm.Configuration.defaultConfiguration = config
メソッドを利用して、デフォルトRealmの設定とすることもできます。
例えば、次のようなアプリケーションを考えます。Web APIによるログイン認証があり、複数のアカウントを切り替えることができるとします。
その場合、アカウントごとにデフォルトRealmの保存場所を設定することで、それぞれのアカウントで別のRealmファイルを使い分けることができます。
func setDefaultRealmForUser(username: String) {
var config = Realm.Configuration()
// 保存先のディレクトリはデフォルトのままで、ファイル名をユーザー名を使うように変更します
config.fileURL = config.fileURL!.deletingLastPathComponent()
.appendingPathComponent("\(username).realm")
// ConfigurationオブジェクトをデフォルトRealmで使用するように設定します
Realm.Configuration.defaultConfiguration = config
}
デフォルト以外のRealm
複数のRealmファイルを別の場所に保存して使い分けることができると便利です。例えば、事前に用意した組み込み済みデータを、メインのデータファイルとは別に読み込み専用のRealmとして利用するなどです。
下記のコードは、アプリケーションに同梱された別のRealmファイルからデータを読み込む例です。
let config = Realm.Configuration(
// アプリケーションバンドルのパスを設定します
fileURL: Bundle.main.url(forResource: "MyBundledData", withExtension: "realm"),
// アプリケーションバンドルは書き込み不可なので、読み込み専用に設定します。
readOnly: true)
// RealmをConfigurationオブジェクト使って作成します
let realm = try! Realm(configuration: config)
// バンドルのRealmからデータを取得します
let results = realm.objects(Dog).filter("age > 5")
Realmを初期化するときに指定するファイルパスは、書き込み可能なディレクトリを指している必要があります。
書き込み可能なディレクトリの中で最も一般的なのは、iOSでは”Documents”フォルダ、OS Xでは”Application Support”フォルダです。
AppleのiOS Data Storage Guidelinesによると、再生成可能なデータに関しては<Application_Home>/Library/Caches
フォルダに保存することが推奨されています。
In-Memory Realm
通常は、Realmはディスクにデータを保存しますが、Realm.Configuration
のfileURL
を指定する代わりにinMemoryIdentifier
を利用することで、データをメモリ上のみに保持するRealmオブジェクトを作ることができます。
let realm = try! Realm(configuration: Realm.Configuration(inMemoryIdentifier: "MyInMemoryRealm"))
In-memory Realmでは、データをディスクに保存しないのでアプリケーションを終了するとデータは消えてしまいます。それ以外のクエリ、リレーションシップ、スレッドセーフなどの機能はすべて通常のRealmと同様に利用することができます。
ディスクへの読み書きによるオーバーヘッドの無い、柔軟なデータアクセス機能が必要なときに有効です。
In-memory Realmはプロセス間通信などに利用するため、一時ディレクトリにいくつかのファイルを作成します。
メモリ不足によりOSがスワップを要求したとき以外は、何のデータも書き込まれません。
注意: スコープを外れ、In-Memory Realmインスタンスへの参照がすべて無くなると、そのRealm内に保存されている、すべてのデータは解放されます。
アプリケーションの起動中は、In-MemoryなRealmインスタンスへの強参照を常に保持しておく必要があります。
エラーハンドリング
一般的なディスクI/Oの処理と同様に、Realm
インスタンスの作成はリソースが不足している環境下では失敗する可能性があります。実際は、各スレッドにおいて最初にRealmインスタンスを作成しようとするときだけエラーが起こる可能性があります。それ以降のアクセスではスレッドごとにキャッシュされたインスタンスが返されるので、失敗することはありません。
エラーを処理してリカバリするには、Swiftの標準のエラー処理の仕組みであるdo〜try〜catch
を利用します。
do {
let realm = try Realm()
} catch let error as NSError {
// handle error
}
Realm間のオブジェクトのコピー
Realmに保存されたオブジェクトを他のRealmへコピーするには、Realm().create(_:value:update:)
メソッドにコピー元のオブジェクトを引数として渡します。 例えば、Realm().create(MyObjectSubclass.self, value: originalObjectInstance)
のように使います。
Realmファイルの探し方
アプリケーション内のRealmファイルの場所がわからないときは、このStackOverflowの回答を参考にしてください。
補助的に作成されるRealmの関連ファイルについて
Realmでは.realm
拡張子を持つメインのRealmファイルとは別に、いくつかの内部的に使用する関連ファイルとディレクトリを自動的に作成します。
.realm.lock
- Realmファイル開く際の競合を防ぐためのロックとして使われます。.realm.management
- プロセス間の競合を防ぐためのロックファイルを格納しています。.realm.note
- スレッド・プロセス間の通知に使用する名前付きパイプのファイルです。
これらのファイルは、.realm
拡張子のRealmのデータファイルには何の影響も与えません。またこれらのファイルを(実行中を除いて)削除したり移動したりすることも問題ありません。
Realmに関する問題を報告する際には、これらの関連ファイルを.realm
拡張子のRealmのデータファイルと一緒に添付してください。関連ファイルには問題を調査するときに役に立つ情報が含まれています。
初期データとしてRealmをアプリケーションにバンドルする
初回起動を素早くするためなどに、アプリケーションに初期データを組み込むことはよくあります。 下記はRealmを初期データとしてバンドルする例です:
- はじめに、初期データの入ったRealmを用意します。 リリースする時と同じデータモデルを定義し、データを入れます。Realmファイルは、クロスプラットフォームで利用可能ですので、データの作成はOS Xや、iOSシミュレータ上で行っても問題ありません。(JSONImportの例をご覧ください)
- 初期データを入れるコードの終わりに、Realmファイルをコピーをするメソッドを使用し、Realm ファイルのサイズを最適化してください(
Realm().writeCopyToPath(_:encryptionKey:)
をご覧ください)。 このメソッドを使ってRealmファイルをコピーすると、Realmのファイルサイズを小さくでき、最終的にアプリケーションのサイズが軽くなるのでユーザが速くダウンロードできます。 - コピーされたRealmファイルを、Xcodeのプロジェクトナビゲーターにドラッグ&ドロップします。
- プロジェクト設定のBuild Phaseタブで、”Copy Bundle Resources”にRealmファイルを追加します。
- この時点で、追加したRealmファイルにアプリケーションからアクセスできるようになります。
NSBundle.main.pathForResource(_:ofType:)
メソッドを使って、ファイルパスを取得します。 -
こうして、
Realm(path:readOnly:encryptionKey:error:)
メソッドで読み込み専用のRealmオブジェクトを作ることもできますし、その初期データを含んだRealmに書き込みたい場合は、NSFileManager.default.copyItemAtPath(_:toPath:error:)
メソッドを使って、アプリケーションのDocuments
ディレクトリにRealmファイルをコピーしてから、Realm(path:)
を使って、Realmオブジェクトを作ります。 -
同梱したRealmデータファイルが固定のデータのみを含んでいて、変更する必要が無いのであれば、
Realm.Configuration
でreadOnly = true
を指定して読み込み専用とし、直接バンドル内のファイルを開くことができます。いっぽう、初期データを変更する必要があるなら、バンドルからドキュメントディレクトリなどにRealmファイルを
NSFileManager.defaultManager().copyItemAtPath(_:toPath:error:)
メソッドでコピーしてから利用します。
Realmファイルを初期データとして組み込む例として、マイグレーションのサンプルコードを参考にしてください。
モデル定義のサブセット
各Realmファイルで保存されるモデルの定義を使い分けたいことがあります。
例えば、内部でRealmを利用する異なるコンポーネントを、2つのチームにより開発しているとします。
その場合、自分の開発しているコンポーネントに関係のないモデルに関するマイグレーション処理はやりたくないことでしょう。
下記のように、Realm.Configuration
オブジェクトのobjectTypes
プロパティを用いて、それぞれのRealmに保存されるモデルクラスを制限することができます。
let config = Realm.Configuration(objectTypes: [MyClass.self, MyOtherClass.self])
let realm = try! Realm(configuration: config)
Realmファイルを削除するには
キャッシュを消去する、データをすべてリセットするなど、いくつかのケースにおいて、Realmファイルを完全にディスクから削除することが適していることがあります。
普通のファイルと異なり、Realmのファイルはメモリにマッピングされており、Realmファイルは{{ RLMRealm }}
インスタンスが生存している間、有効でなければなりません。
普通のファイルはNSFileManager
のremoveItemAtPath
メソッドを用いて削除することができますが、Realmファイルを削除するには、削除対象のファイルにアクセスしている{{ RLMRealm }}
インスタンスの強参照が1つもない状態にしなければなりません。
Realm
インスタンスは生きている間、データベースへの接続を保持します。Realm インスタンスの生存期間を制限する方法の1つは、オートリリースプールを使って囲むことです。
つまり、削除を実行する前に、削除対象のファイルにアクセスしているすべての{{ RLMRealm }}
インスタンスを解放する必要があります。
最後に、これは厳密には必須ではありませんが、完全にすべてのファイルを削除しようとするなら、補助的に作成されるRealmの関連ファイルも同様に削除しましょう。
autoreleasepool {
// all Realm usage here
}
let realmURL = Realm.Configuration.defaultConfiguration.fileURL!
let realmURLs = [
realmURL,
realmURL.appendingPathExtension("lock"),
realmURL.appendingPathExtension("log_a"),
realmURL.appendingPathExtension("log_b"),
realmURL.appendingPathExtension("note")
]
let manager = NSFileManager.defaultManager()
for URL in realmURLs {
do {
try FileManager.default.removeItem(at: URL)
} catch {
// handle error
}
}
Appのバックグラウンド更新でRealmにアクセスする場合
iOS 8以降では、アプリが作成したファイルはデバイスがロックされている間はNSFileProtection
APIの機能により、自動的に暗号化されます。
もしデバイスのロック中にアプリがRealmにアクセスする場合は、デフォルトではNSFileProtection
属性が暗号化するように設定されているため、open() failed: Operation not permitted
というメッセージの例外が発生します。
この問題を解決するためには、Realmと補助的に作成されるRealmの関連ファイルすべてに対して、デバイスのロック中でもファイルにアクセスできるように、ファイル保護の属性を緩める必要があります。例えばNSFileProtectionCompleteUntilFirstUserAuthentication
などを指定します。
もし、上記のようにしてiOSのファイル保護機能を使わないように設定するのであれば、データを保護するためにRealmの暗号化機能を利用することをおすすめします。
Realmの関連ファイルは遅れて作成されたり、途中で削除されることもあるので、ファイル保護の属性はRealmファイルを保存しているフォルダに対して設定することをおすすめします。そうすることで、Realmと関連ファイルすべてに同じファイル保護の属性を簡単に設定でき、ファイルが遅れて作成される問題にも対処できます。
let realm = try! Realm()
// Realmファイルを保存するフォルダのパスを取得します
let folderPath = realm.configuration.fileURL!.deletingLastPathComponent().path
// Realmファイルを保存するフォルダに対してファイル保護を無効にします
try! FileManager.default.setAttributes([FileAttributeKey(rawValue: NSFileProtectionKey): NSFileProtectionNone],
ofItemAtPath: folderPath)
マルチスレッド
独立したスレッドでRealmを使っている限りは、Realmのオブジェクトはすべて一般的なオブジェクトと同じように扱え、並行処理やマルチスレッドについて気にする必要はありません。 Realmにアクセスするためにロックや排他処理を考える必要はありません(たとえRealmが他のスレッドから同時に更新されるとしても)。そして、データの変更が起こるのはトランザクションブロックで囲まれた範囲だけと考えられます。
Realmは並行処理を簡単に扱えるようにするために、それぞれのスレッドで常に一貫性を保ったデータを返します。数多くのスレッドからRealmを同時に操作したとしても、それぞれのスレッドごとにスナップショットのデータが返され、不整合な状態が見えることがありません。
1つだけ注意しなければならないことは、複数のスレッドをまたいで同じ Realmインスタンス を共有することはできないということです。もし、複数のスレッドで同じオブジェクトにアクセスする必要がある場合は、それぞれのスレッドでRealmインスタンスを取得する必要があります。(そうでなければデータが不整合に見える可能性があります。)
他のスレッドから更新されたデータを反映するには
メインスレッド(またはランループを持つサブスレッド)では、ランループが回るごとに自動的に他のスレッドから更新されたデータが反映されます。それ以外のタイミングでは、その時点のスナップショットのデータが返され、他のスレッドでデータが更新されているかどうかを気にすることなく、常に一貫性のあるデータが見えることになります。
それぞれのスレッドで最初にRealmファイルをオープンしたとき、Realmの状態は最後のコミットが成功した状態にあります。そしてそれは次の更新が反映されるまでそのままです。 Realmはautorefresh
がNO
でない限りは、ランループが回るたびに自動的に最新のデータに更新されます。 もしスレッドがランループを持っていない(一般的なバックグラウンドスレッド)場合は、最新のデータを反映するためにRealm.refresh()
メソッドを自分で呼ぶ必要があります。
また、トランザクションがコミットされたときも最新のデータが反映されます(Realm.commitWrite()
)。
定期的に行われる最新データの反映が失敗すると、トランザクション履歴が”Pinned”になり、ディスク領域の再利用を妨げます。それはファイルサイズの肥大を招くことがあります。この現象について詳しくは現バージョンにおける制限事項をご覧ください。
スレッド間でオブジェクトを受け渡す
アンマネージド(Unmanaged、Realmに追加される前の)Object
のインスタンスは、通常のSwiftのオブジェクトと同様に振る舞います。スレッドをまたいでオブジェクトを渡しても問題ありません。
Realm
、Results
、Object
および、マネージド(Managed、Realmに追加された後の)Object
のインスタンスは、生成されたスレッド内でなければ利用することができません。 別のスレッドで利用されると例外が発生します。これはRealmがトランザクションを分離するための必要な仕様です。
スレッド間をまたいでオブジェクトを受け渡すための方法を紹介します。例えば、プライマリキーやそのオブジェクトにマッチする検索条件(NSPredicate
もしくはクエリ文字列)、またはRealm.Configuration
を用いて、各スレッドごとにObject
インスタンスを取得する方法があります。対象のスレッドで取得したインスタンスは、もともとのスレッドとは別の時点のトランザクションのデータである可能性があるので、注意してください。
いくつかのプロパティとメソッドは別のスレッドからでもアクセス可能です。
Realm
: すべてのプロパティ、クラスメソッド、イニシャライザObject
:invalidated
、objectSchema
、realm
、クラスメソッド、イニシャライザResults
:objectClassName
、realm
Object
:invalidated
、objectClassName
、realm
複数スレッド間でRealmを使う
同じRealmファイルを複数のスレッドから使う場合は、各スレッドでRealmインスタンスをそれぞれ生成する必要があります。 同じ設定によって作られたRealmインスタンスは、ディスク上で同じものを指します。
複数のスレッド間で、Realmインスタンスを共有することはサポートされていません。同じRealmファイルにアクセスするRealmインスタンスは、すべて同じ設定(Realm.Configuration
)でなければなりません。
Realmは、大量のデータを追加するときには、一つのトランザクション中に複数の一括更新をすることにより、非常に効率よく動作します。
また、メインスレッドをブロックすることを避けるためGrand Central Dispatchを使い、トランザクションをバックグラウンドで実行することができます。
Realm
オブジェクトはスレッドセーフではないため、複数のスレッド間で共有することができません。それぞれのスレッド/ディスパッチキューでRealm
オブジェクトを生成する必要があります。
下記は、バックグランド処理で100万個のオブジェクトを追加する例です。
DispatchQueue(label: "background").async {
autoreleasepool {
// このスレッドで使うRealmインスタンスを取得します
let realm = try! Realm()
// トランザクションを開始して、
// 1000件単位で書き込みます
for idx1 in 0..<1000 {
realm.beginWrite()
// ディクショナリからオブジェクトを作成する場合は、プロパティの順序を気にする必要はありません
for idx2 in 0..<1000 {
realm.create(Person.self, value: [
"name": "\(idx1)",
"birthdate": Date(timeIntervalSince1970: TimeInterval(idx2))
])
}
// トランザクションを込みとして、
// 他のスレッドのRealmからデータを利用できるようにします。
try! realm.commitWrite()
}
}
}
JSON
RealmはJSONを直接的にはサポートしていません。ですが、NSJSONSerialization.JSONObjectWithData(_:options:)
の戻り値からObject
を生成することができます。 言い換えると、KVC準拠のオブジェクトであれば、Object
を保存、更新する際のメソッドに渡すことができ、オブジェクトを保存したり更新することができます。
// A Realm Object that represents a city
class City: Object {
dynamic var city = ""
dynamic var id = 0
// other properties left out ...
}
let data = "{\"name\": \"San Francisco\", \"cityId\": 123}".data(using: .utf8)!
let realm = try! Realm()
// JSONを使ってオブジェクトを保存します
try! realm.write {
let json = try! JSONSerialization.jsonObject(with: data, options: [])
realm.create(City.self, value: json, update: true)
}
JSONがさらにネストしたオブジェクトや配列を含む場合、自動的に1対1か1対多の関連としてマッピングされます。詳しくはネストしたオブジェクトのセクションをご覧ください。
この方法によってRealmにオブジェクトを保存、更新する場合は、JSONのプロパティ名と型は、対象となるObjectのプロパティと型に完全に一致している必要があります。例えば、
float
型のプロパティはfloat
型を格納するNSNumber
型である必要があります。NSDate
型やNSData
型のプロパティは文字列から自動的に変換されません。Realm().create(_:value:update:)
メソッドに渡す前に変換しておく必要があります。- 必須のプロパティにJSONから
null
(例:NSNull
)が渡された場合は例外が発生します。 - 必須のプロパティに値が渡されなかった場合は例外が発生します。
- JSON側に
Object
に定義されていないプロパティがあった場合は無視します。
もし利用しているJSONスキーマが完全にRealmオブジェクトの定義と一致していない場合は、サードパーティ製のマッピングライブラリをJSONの変換に使用することを推奨します。Swiftには多数の積極的にメンテナンスされているマッピングライブラリがあり、Realmと一緒に使用できます。こちらのIssueにいくつか一覧にまとめてあります。
通知
addNotificationBlock
メソッドでブロックを登録すると、Realm
、Results
、List
またはLinkingObjects
オブジェクトが更新されたとき、通知を受けることができます。
個別のObject
の変更を監視するにはキー値監視(KVO)が利用できます。
通知が即座に送られずに、複数のトランザクションの変更が1つの通知にまとめられることがあります。
戻り値の“Notificaiton Token”が保持されている限り、通知は有効です。 更新の通知を登録したクラスの“Notificaiton Token”の強参照を保持しておく必要があります。 “Notificaiton Token”が解放されると、登録された通知は自動的に解除されます。
Realmに対する通知
Realmインスタンスは、他のスレッドでトランザクションが完了するたびに、他のRealmインスタンスに対して通知を送ります:
// Realmの通知を監視するように登録します
let token = realm.addNotificationBlock { notification, realm in
viewController.updateUI()
}
// 通知が不要になったら
token.stop()
コレクションに対する通知
コレクションに対する通知はRealmに対する通知とは少し異なります。コレクションに対する通知では、この変更で実際に何が変わったのかがインデックスの配列を用いて通知されます。インデックスの配列には、1つ前に通知を受けたときから、追加、削除、および変更されたオブジェクトのインデックスが格納されています。
コレクションに対する通知は、非同期で通知されます。初回の通知はクエリが実行完了された時点で呼ばれます。そのあとは、コレクションに格納されているオブジェクトに変更、または追加が発生するトランザクションがコミットされるたびに通知されます。
変更内容は通知ブロックに渡されるRealmCollectionChange
パラメータを用いて知ることができます。このオブジェクトは変更があったインデックスの情報をdeletions
、insertions
、modifications
という変数で保持しています。
最初の2つ、deletionsとinsertions変数はコレクションに対してオブジェクトを追加・削除するたびに変更された部分のインデックスを記録しています。 Results
の場合は、検索条件に関する値が変更されて、検索条件に新しくマッチするオブジェクトが増えたとき、あるいはマッチしなくなってオブジェクトが減ったときに変更として扱われます。
Results
の派生クラスを含むList
またはLinkingObjects
といったコレクションについては、さらに関連にオブジェクトが追加されたときと、関連からオブジェクトが削除されたときにも変更として扱われます。
modificationsはオブジェクトのプロパティが変更されたときにインデックスが含まれます。それは1対1や1対多の関連が変更された場合も当てはまります。ただし、逆方向の関連の変更の場合は含まれません。
class Dog: Object {
dynamic var name = ""
dynamic var age = 0
}
class Person: Object {
dynamic var name = ""
let dogs = List<Dog>()
}
上記のようなモデルクラスが定義されているとき、dogsの所有者であるPersonオブジェクトの変更を監視するとします。その場合は、検索条件にマッチするPerson
オブジェクトに下記の変更が加えられた場合に、通知が届きます。
Person
オブジェクトのname
プロパティを変更したときPerson
オブジェクトのdogs
プロパティに要素を追加または削除したときPerson
オブジェクトと関連づけがあるDog
オブジェクトのage
プロパティを変更したとき
このようなきめ細やかな通知の仕組みにより、変更があった際にただすべてを再読み込みするだけでなく、より分かりやすいアニメーションや表示を伴ったUIの更新を行うことができます。
class ViewController: UITableViewController {
var notificationToken: NotificationToken? = nil
override func viewDidLoad() {
super.viewDidLoad()
let realm = try! Realm()
let results = realm.objects(Person).filter("age > 5")
// Resultsの通知を監視します
notificationToken = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
break
case .update(_, let deletions, let insertions, let modifications):
// Resultsに変更があったので、UITableViewに変更を適用します
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
break
case .error(let error):
// バックグラウンドのワーカースレッドがRealmファイルを開く際にエラーが起きました
fatalError("\(error)")
break
}
}
}
deinit {
notificationToken?.stop()
}
}
ユーザー主導による変更に対応する
Realmの通知はモデルレイヤーに対するすべての変更に対応するように設計されています。その変更が別のスレッドやプロセスによるものであっても同じです。
ほとんどのアプリケーションはユーザーが直接UIとデータを同時に変更することができるようになっています。
一方で、サーバーのレスポンスをバックグラウンドスレッドで受信してモデルを更新するなど、モデルだけに対する変更も発生します。この場合は、UIによるモデルの変更とは逆に、モデルの変更をUI側にも反映しなければなりません。
UITableView
におけるテーブルビューせるの並べ替えはこのパターンの一般的な例でしょう。Results
をUITableViewDataSource
として使っているとして、Resultsに対する変更をコレクションに対する通知を用いて監視しています。
ユーザーはセルをドラッグして並べ替えることができます。その際、UIの変更に合わせてUITableViewDataSource
を変更しなければなりません。そのため、変更された内容をモデルで更新します。そのトランザクションがコミットされたとき、通知が呼ばれてしまいます。しかし、コレクションに対する通知ブロックでこの変更を再度UIに反映してしまってはUIが崩れてしまいます。なぜならその変更はすでにユーザーの操作によって行われているからです。
このような重複する更新を避ける必要があります。通知は即座に送られずに幾つかのコミットがまとめられることがありますので、必要な通知だけをうまくフィルタする汎用的な方法はありません。
このような理由により、通知ブロックの通知によって、重複したUIの更新をしてしまわないように、UIの取り扱いには特に注意する必要があります。この問題を避けるためにはいくつかのテクニックがあります。1つはユーザーがデータを操作している間はトランザクションをオープンしたままにしておくことです。その際、書き込みトランザクションは他の書き込みをブロックすることに留意してください。もう一つは、モデルオブジェクトの変更がユーザーの操作によるものなのか、バックグラウンドインポートのようなユーザーの操作を伴わないものなのかを区別するためのフラグを持つ方法です。
このことは難しい設計の問題として認識しており、よりエレガントな解決方法を模索している段階です。(#3665)
通知に関するAPI
通知に関して詳しくは、下記に示すAPIドキュメントをご覧ください:
Realm.addNotificationBlock(_:)
AnyRealmCollection.addNotificationBlock(_:)
Results.addNotificationBlock(_:)
List.addNotificationBlock(_:)
NotificationToken.stop()
キー値監視(KVO)
Realmオブジェクトのほとんどのプロパティはキー値監視(KVO)に対応しています。
すべてのObject
のサブクラスにおける永続化対象(保存しないプロパティではない)のプロパティはKVOに対応しています。永続化される前のinvalidated
なObject
型とList
型のプロパティも同様です。(LinkingObjects
型のプロパティはKVOには対応していません。)
Realmに追加される前(アンマネージド)のObject
のサブクラスのオブジェクトに対するキー値監視は、一般的なダイナミックプロパティと同様に動作します。
しかし、監視対象のオブジェクトは、監視対象として登録されている間はrealm.add(obj)
などのメソッドでRealmに保存することができないので注意してください。
マネージドオブジェクト(Realmに追加された後のオブジェクト)に対するキー値監視は少し異なる動作をします。
マネージドオブジェクト(Realmに追加された後のオブジェクト)においては、そのプロパティの変わるタイミングが3種類あります。プロパティに値を直接代入したとき、realm.refresh()
メソッドを呼び出したとき、または別のスレッドがトランザクションをコミットして自動的にRealmが更新されたとき、そして別のスレッドから変更があったがその前にrealm.beginWrite()
を呼び出してトランザクションを開始したために変更が通知されなかったときです。
直接代入する以外の2つのケースではすべての別スレッドから行われた変更は、一度に適用されます。そのため、KVOの通知は1回にまとめられます。途中の変更の状態は破棄されるので、1から10まで1つずつ数を増加させるプロパティがあった場合、10に変化する際の1つの通知だけを受け取ることになります。
トランザクションの外でプロパティの変更が行われる可能性があるので、-observeValueForKeyPath:ofObject:change:context:
メソッドの中でマネージドRealmオブジェクトを変更することは推奨されません。
NSMutableArray
のプロパティとは異なり、Object
のプロパティに対する変更を監視するには-mutableArrayValueForKey:
を使う必要はありません(互換性のためにそのメソッドを利用しても同様に機能するようにはしていますが)。直接Object
を変更するメソッドを呼び出すだけで(監視していれば)更新が通知されます。
一般のプロパティと異なり、RealmのList
プロパティはKVOの監視対象とするためにdynamic
として定義する必要はありません。
サンプルコードに簡単なRealmとReactiveCocoa(Objective‑C)のサンプル、およびReactKit(Swift)のサンプルがありますので参考にしてください。
マイグレーション
データベースを使ってる場合、時間が経つにつれ、データモデルは変更されていくものです。 Realmでのデータモデルは、シンプルなSwiftインターフェースで定義されていますので、インターフェースに変更を加えるだけで、簡単にデータモデルを変えられます。 例えば、以下のPerson
モデルについて考えてみてください。
class Person: Object {
dynamic var firstName = ""
dynamic var lastName = ""
dynamic var age = 0
}
ここで、firstName
とlastName
を一つにして、fullName
プロパティが必要になったとします。 そこで以下のような単純な変更をインターフェースに加えることにします。
class Person: Object {
dynamic var fullName = ""
dynamic var age = 0
}
ここでのポイントは、もし以前に前のデータモデルでのデータが保存されている場合、新しく定義し直したデータモデルとディスクに保存されている古いデータモデルとの間に不整合が生じてしまいます。 マイグレーションを実行せずにRealmを使おうとすると、例外が発生します。
マイグレーションを実行する
マイグレーションの定義
マイグレーション処理は、Realm.Configuration.migrationBlock
を利用して定義します。
スキーマのバージョンはRealm.Configuration.schemaVersion
を用いて設定します。
マイグレーションブロックの中には、すべての古いデータモデルから新しいデータモデルへ移行させるためのロジックが書かれていなければなりません。
上記の設定オブジェクトを用いてRealm
オブジェクトが作られたときに、スキーマバージョンが異なっていればマイグレーション処理が適用されます。
たとえば、上記のPerson
クラスのマイグレーションについて考えてみましょう。 最低限必要なマイグレーション処理は、以下のようなものです。
// application(application:didFinishLaunchingWithOptions:)の中に書きます
let config = Realm.Configuration(
// 新しいスキーマバージョンを設定します。以前のバージョンより大きくなければなりません。
// (スキーマバージョンを設定したことがなければ、最初は0が設定されています)
schemaVersion: 1,
// マイグレーション処理を記述します。古いスキーマバージョンのRealmを開こうとすると
// 自動的にマイグレーションが実行されます。
migrationBlock: { migration, oldSchemaVersion in
// 最初のマイグレーションの場合、`oldSchemaVersion`は0です
if (oldSchemaVersion < 1) {
// 何もする必要はありません!
// Realmは自動的に新しく追加されたプロパティと、削除されたプロパティを認識します。
// そしてディスク上のスキーマを自動的にアップデートします。
}
})
// デフォルトRealmに新しい設定を適用します
Realm.Configuration.defaultConfiguration = config
// Realmファイルを開こうとしたときスキーマバージョンが異なれば、
// 自動的にマイグレーションが実行されます
let realm = try! Realm()
Realmによって自動的にスキーマが更新されることを示すために、ここでは空のブロックでマイグレーションを実行しています。
値の更新
これは最低限のマイグレーションですが、おそらく何かデータを新しいプロパティ(ここではfullName
)に入れるために、ここに処理を記述すると思います。
マイグレーションブロックの中では、特定の型の列挙を行うためにMigration().enumerateObjects(ofType: _:_:)
を呼ぶことができます。
下記では、必要なマイグレーションロジックを適用しています。 変数oldObject
を使って既にあるデータにアクセスし、新しく更新するデータには変数newObject
を使ってアクセスしています。
// application(application:didFinishLaunchingWithOptions:)の中に書きます
Realm.Configuration.defaultConfiguration = Realm.Configuration(
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
if (oldSchemaVersion < 1) {
// enumerateObjects(ofType:_:)メソッドで保存されているすべての
// Personオブジェクトを列挙します
migration.enumerateObjects(ofType: Person.className()) { oldObject, newObject in
// firstNameとlastNameをfullNameプロパティに結合します
let firstName = oldObject!["firstName"] as! String
let lastName = oldObject!["lastName"] as! String
newObject!["fullName"] = "\(firstName) \(lastName)"
}
}
})
一度マイグレーション処理が適用されると、その後は通常どおりにRealmとRealmオブジェクトが使用できます。
プロパティ名の変更
プロパティ名を変更した場合は、マイグレーション時にはプロパティ名の変更という操作を実行する方が、値や関連をコピーするよりも効率的に動作します。
マイグレーション時にプロパティを変更する場合は、変更後のプロパティ名がモデルに存在することと、変更前のプロパティ名がモデルに存在しないことを確認してください。
変更後のプロパティが変更前と異なるNULL可/不可属性、インデックスの設定を持つ場合は、名前の変更の際に適用されます。
以下は、Person
クラスのyearsSinceBirth
プロパティをage
に変更する例です。
// application(application:didFinishLaunchingWithOptions:)の中に書きます
Realm.Configuration.defaultConfiguration = Realm.Configuration(
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
// 初めてマイグレーションを実行するのでoldSchemaVersionは0です
if (oldSchemaVersion < 1) {
// 名前の変更は`enumerateObjects(ofType: _:)`の外側で実行する必要があります。
migration.renameProperty(onType: Person.className(), from: "yearsSinceBirth", to: "age")
}
})
バージョンの追加方法
Person
クラスについて、以前に異なる2つのデータモデルをとっていた場合を考えてみましょう。
// v0
// class Person: Object {
// dynamic var firstName = ""
// dynamic var firstName = ""
// dynamic var age = 0
// }
// v1
// class Person: Object {
// dynamic var fullName = "" // 追加したプロパティ
// dynamic var age = 0
// }
// v2
class Person: Object {
dynamic var fullName = ""
dynamic var email = "" // 追加したプロパティ
dynamic var age = 0
}
ここでのマイグレーションブロックの中に書くロジックは、下記のようになります。
Realm.Configuration.defaultConfiguration = Realm.Configuration(
schemaVersion: 2,
migrationBlock: { migration, oldSchemaVersion in
// enumerate(_:_:)メソッドで保存されているすべての
// Personオブジェクトを列挙します
migration.enumerateObjects(ofType: Person.className()) { oldObject, newObject in
// スキーマバージョンが0のときだけ、'fullName'プロパティを追加します
if oldSchemaVersion < 1 {
let firstName = oldObject!["firstName"] as! String
let lastName = oldObject!["lastName"] as! String
newObject!["fullName"] = "\(firstName) \(lastName)"
}
// スキーマバージョンが0または1のとき、'email'プロパティを追加します
if oldSchemaVersion < 2 {
newObject!["email"] = ""
}
}
})
// Realmは自動的にマイグレーションを実行し、成功したらRealmを開きます。
let realm = try! Realm()
完全なマイグレーション処理のコードについては、マイグレーション処理のサンプルコードをご覧ください。
複数世代のマイグレーション
二人のユーザーがいると考えてください。JPとTimです。
JPはよくアプリケーションをアップデートします。しかしTimはいくつかのバージョンをスキップすることがあります。
JPはすべてのバージョンをインストールしてることになるので、段階的にすべてのスキーマのアップデートを適用していることになります。V0からV1、V1からV2のようにスキーマはアップデートしていきます。
対照的に、Timの場合はV0からV2と突然バージョンがジャンプする可能性があります。
マイグレーションブロックは、たとえどのバージョンのスキーマから始めたとしても、ネストしていないif (oldSchemaVersion < X)
で必要なアップデート処理が行われるように構成しなければなりません。
また、他のケースで、ユーザーがバージョンをスキップをしてアプリケーションをアップデートしたとします。もしあなたが”email”プロパティを一度バージョン2で削除し バージョン3で再び追加したとします。
そしてユーザーがバージョン1からバージョン3へと、バージョン2をスキップしてアップデートした場合、Realmはコードのスキーマ定義とディスクのスキーマに違いは無いので、”email”プロパティが一度消されたということを自動的に判別することはできません。これは、Tim の Person クラスで起こりうることで、バージョン1時点のプロパティの値をバージョン3バージョンのプロパティの値として保持される可能性があります。
このことは、内部的に”email”プロパティの表現形式を変えるなどしていなければ、問題にならないかもしれません(ISO形式のメールアドレスから独自の形式にするなどです。)。
しかし、問題を避けるためif (oldSchemaVersion < 3)
の中で、データセットがバージョン3バージョンのものであることを保証するために、一度nilを代入することを推奨します。
暗号化
Realmの暗号化APIはiOS、OS XおよびWatchKitで利用できます。ただし、watchOSでは利用できません。
なぜなら、暗号化の仕組みとして使用している<mach/mach.h>
と<mach/exc.h>
のAPIが__WATCHOS_PROHIBITED
となっているためwatchOSでは利用できないからです。
Appleに不具合として報告しています: rdar://22063654。
Please take note of the Export Compliance section of our LICENSE, as it places restrictions against the usage of Realm if you are located in countries with an export restriction or embargo from the United States.
Realmでは64バイトの暗号化キーを用いてAES-256とSHA-2暗号化方式でデータベースファイルを暗号化する機能を提供しています。
// ランダムな暗号化キーを生成します
var key = Data(count: 64)
_ = key.withUnsafeMutableBytes { bytes in
SecRandomCopyBytes(kSecRandomDefault, 64, bytes)
}
// 暗号化されたRealmファイルを開きます
let config = Realm.Configuration(encryptionKey: key)
do {
let realm = try Realm(configuration: config)
// 使い方は暗号化なしのRealmと変わりません
let dogs = realm.objects(Dog).filter("name contains 'Fido'")
} catch let error as NSError {
// もし暗号化キーが間違っている場合、`error`オブジェクトは"invalid database"を示します
fatalError("Error opening realm: \(error)")
}
この機能を使用すると、ディスクに保存されるデータが透過的にAES-256で必要に応じて暗号/複合化され、SHA-2 HMACによって検証されます。
暗号化したRealmファイルのインスタンスを作成するには同じ暗号化キーが必要になります。
詳しくは暗号化のサンプルコードをご覧ください。暗号化キーの作り方、キーチェーンへの安全な保存方法、Realmへのキーの渡し方などが理解できます。
暗号化したRealmを使う場合、わずかにパフォーマンスが下がり(10%未満)ます。
サードパーティのクラッシュレポートツール(CrashlyticsやPLCrashReporterなど)は、暗号化したRealmを初めて開く前に登録する必要があります。そうしないと、アプリが実際にクラッシュしていないにもかかわらず、誤ったクラッシュレポートを受け取る可能性があります。
テスト
デフォルトRealmの設定を変更する
Realmを使用する(実際のアプリケーションで使用する場合でも、テストで使用する場合でも)ときの最も簡単な方法は、デフォルトRealmを使用することです。
実際のアプリケーションのデータを上書きしてしまうことを防ぐため、あるいは各テストの状態が他のテストに影響することを防ぐために、それぞれのテストで新しいRealmファイルを使うようにデフォルトRealmを設定します。
import XCTest
// A base class which each of your Realm-using tests should inherit from rather
// than directly from XCTestCase
class TestCaseBase: XCTestCase {
override func setUp() {
super.setUp()
// テストケース名で区別して、テストごとにIn-memoryなRealmを使うようにします。
// こうすることで、テストによってアプリケーションのデータを変更してしまうことと、
// 他のテストに影響が及ぶことを防ぎます。
// そして、In-memoryなRealmを使うので
// 後始末として、データを削除する必要はありません。
Realm.Configuration.defaultConfiguration.inMemoryIdentifier = self.name
}
}
外部からRealmインスタンスを渡す
別の方法としては、すべてのRealmに関係するコードのメソッドを、Realm
インスタンスを受け取るようにします。アプリケーションの実行時とテスト実行時で、異なるRealmインスタンスを渡すようにします。
例えば、以下のテストコードは、JSON APIからユーザー情報を取得し、正しくデータが保存されているかをテストしています。
// Application Code
func updateUserFromServer() {
let url = URL(string: "http://myapi.example.com/user")
URLSession.shared.dataTask(with: url!) { data, _, _ in
let realm = try! Realm()
createOrUpdateUser(in: realm, with: data!)
}
}
public func createOrUpdateUser(in realm: Realm, with data: Data) {
let object = try! JSONSerialization.jsonObject(with: data) as! [String: String]
try! realm.write {
realm.create(User.self, value: object, update: true)
}
}
// Test Code
let testRealmURL = URL(fileURLWithPath: "...")
func testThatUserIsUpdatedFromServer() {
let config = Realm.Configuration(fileURL: testRealmURL)
let testRealm = try! Realm(configuration: config)
let jsonData = "{\"email\": \"help@realm.io\"}".data(using: .utf8)!
createOrUpdateUser(in: testRealm, with: jsonData)
let expectedUser = User()
expectedUser.email = "help@realm.io"
XCTAssertEqual(testRealm.objects(User.self).first!, expectedUser,
"User was not properly updated from server.")
}
デバッグ
Realm Swift APIを使っている場合、アプリケーションのデバッグはLLDBコンソールを使う必要があります。
注意することとして、Realmの変数を調べるLLDBスクリプトをXcode Plugin経由でインストールしていたとしても、Swiftの場合これは正しく機能せず実際と異なるデータが表示されます。代わりにLLDBコンソールでpo
コマンドを使ってRealmに保存されたデータを調べてください。
Realm.frameworkとテストターゲットをリンクしない
ダイナミックフレームワークとしてRealmを使用している場合、ユニットテストのターゲットがRealmフレームワークを見つけられるようにしておく必要があります。 そのためには、ユニットテストの”Framework Search Paths”にRealmSwift.framework
の親フォルダの場所を追加します。
テスト実行時に"Object type 'YourObject' is not managed by the Realm"
というエラーが出る場合、RealmSwift.framework
がテストターゲットにリンクされていることが原因です。 問題を解決するために、テストターゲットからRealmのリンクを解除してください。
また、モデルクラスのファイルはアプリケーションのターゲットか、フレームワークのターゲットのどちらか一方にのみリンクされている必要があります。 そうなってなければ、テスト時にモデルクラスに重複が生じて問題が発生する可能性があります。 詳しくはこちらのIssueをご覧ください。
テスト対象のコードは、全てテストターゲットからアクセス可能である必要があります。(public
修飾子を使うか、@testable
を使います。)詳しくはこちらのStack Overflowの回答をご覧ください。
既知の制限事項
現在のバージョンの制限事項として下記に挙げる問題があります。
既知の問題について網羅的に知りたい場合はGitHub Issuesをご覧ください。
一般的な制限事項
Realmは、柔軟性とパフォーマンスのバランスをうまく保つため、保存するデータに対していくつかの制限事項があります。
- クラス名は57文字が上限です。
- プロパティ名は63文字が上限です。
NSData
型およびString
型のプロパティは、16MB以上のデータを保存することはできません。 それ以上のデータを保存するには、16MB以下の複数のデータにに分割するか、ファイルとして保存し、Realmにはファイルパスを記録します。 16MB以上のデータを保存しようとすると、実行時に例外が投げられます。- 各Realmファイルのサイズは、アプリケーションごとにに割り当てられるメモリサイズを超えてはいけません。割り当てられるメモリサイズは、デバイスごとに異なり、実行時のメモリの断片化にも依存します。(詳しくは、rdar://17119975 をご覧ください)それ以上のデータを保存される場合は、Realmファイルを複数に分割してください。
- 文字列による検索結果の並べ替え、および大文字小文字を無視する検索条件は、’基本ラテン文字’、’ラテン1補助’、’ラテン文字拡張A’、’ラテン文字拡張B’においてのみ動作します。(UTF-8の範囲は、0-591)
マルチスレッド
Realmファイルは複数のスレッドから同時にアクセスすることができますが、RealmやRealmオブジェクトのインスタンス、クエリ、Resultsオブジェクトをスレッドをまたいで受け渡すことはできません。Realmのスレッドモデルについて詳しくはこちらをご覧ください。
モデルクラスのsetterおよびgetterメソッドはオーバーライドできません
Realmは、データベースのプロパティとデータベースの操作を連動させて、遅延ロードや高速な性能を実現するために、モデルクラスのsetterおよびgetterをオーバーライドしています。そのため、Realmモデルクラスではプロパティのsetterおよびgetterメソッドをオーバーライドすることはできません。
簡単な解決方法は保存しないプロパティとして宣言することです。保存しないプロパティのsetterおよびgetterメソッドは自由にオーバーライドすることが可能です。
ファイルサイズと中間データについて
SQLiteなどにデータを保存した時より、ディスクの使用容量が少なくなることを期待されることかと思います。 Realmファイルの容量が考えているよりも大きい場合は{{ RLMRealm }}
は古い履歴データを残している可能性があります。
データの一貫性を保つために、Realmは最新のデータにアクセスしたときのみ履歴をアップデートします。 このことは、別のスレッドが多くのデータを長い時間をかけて書き込んでいる最中にデータを読み出そうとした場合、履歴はアップデートされずに古いデータを読み出すことになります。結果として、履歴の中間データが増加していくことになります。
この余分な領域は、最終的には再利用されるか消去されます。(強制的に空き領域を消去するには、 Realm().writeCopyToPath(_:encryptionKey:)
を使ってファイルをコピーします。そのとき、自動的にファイルサイズが最適化されます。)
この問題を避けるには、invalidate
メソッドを呼び出し、Realmにこれまでに取得したデータはもう必要なくなったことを知らせてください。 そうすると、Realmは中間データの履歴を解放します。そして、次のアクセスのときに最新のデータを使うようにRealmが更新されます。
また、GCDを使ってRealmにアクセスしたときにも、この問題が発生する可能性があります。ブロックの実行が終了した後も、オートリリースプールのオブジェクトが解放されずに、{{ RLMRealm }}
が解放されるまで古い履歴のデータが残ることによります。
この問題を避けるためにGCDのブロック内でRealmにアクセスするときは、明示的にオートリリースプールを利用してください。
オートインクリメント機能はサポートしていません
Realmは他の一般的なデータベースが備えている(スレッドセーフまたはプロセスセーフな)オートインクリメントなプライマリキーを生成する機構を備えていません。しかし、自動生成されるユニークキーが連番であることや、連続していること、数値であることが必須要件であることは、たいていの場合ありません。
たいていの場合は、ユニークな文字列をプライマリキーとすることでこと足ります。よくあるパターンはデフォルト値として NSUUID().UUIDString
を使い、ユニークな文字列を生成します。
オートインクリメントが求められるケースとしては、挿入順を保持しておきたいという場合があります。このようなときは、オブジェクトの保持にObject
を使うか、createdAt
のようなプロパティをモデルに追加し、デフォルト値としてNSDate()
を使用しましょう(そしてcreatedAt
でソートします)。
Objective‑CからList型およびRealmOptional型のプロパティへのアクセスはできません
回避策として、Realm SwiftのモデルをObjective‑Cから使用する場合はList
型とRealmOptional
型のプロパティに対して、@nonobjc
アノテーションを指定する必要があります。 これは、Genericsを使っているプロパティがあると、コンパイルできないObjective‑Cヘッダ(-Swift.h
)が自動生成されてしまうというSwiftの不具合によります。
モデルクラスにカスタム定義のイニシャライザを追加する
モデルクラスをObject
のサブクラスとして定義する際に、カスタムのイニシャライザを定義したくなることがあるでしょう。
Swiftのイントロスペクション機能の制限により、カスタムイニシャライザは指定イニシャライザ(Designated Initializer)にできません。そのため、convenience
キーワードを使い、コンビニエンスイニシャライザ(Convenience Initializer)として指定する必要があります。
class MyModel: Object {
dynamic var myValue = ""
convenience init(myValue: String) {
self.init() //Please note this says 'self' and not 'super'
self.myValue = myValue
}
}
レシピ
実際のプロジェクトでよく求められる機能を、Realmを使って作成するためのレシピをまとめています。定期的に新しいレシピを追加する予定ですので、ときどきチェックしてください。追加してほしい例がありましたら、GitHubのIssueに書いてください。
- Building a To-Do App with Realm
- Testing Realm Apps
- Sharing Data between WatchKit & your App with Realm
- Building an iOS Clustered Map View in Swift
- Building an iOS Search Controller in Swift
- Building a Grid Layout with UICollectionView and Realm Swift
- Unidirectional Data Flow in Swift
Realm Mobile Platform
Realm Mobile PlatformはRealm Mobile Databaseを拡張し、ネットワークを通じてデバイス間の自動的なデータ同期を実現します。同期されたRealmに対応するために、いくつかの新しいクラスが導入されました。下記以降でRealm Mobile Databaseに新しく同期のために追加されたクラスを説明します。
Userクラス
Realmユーザー(SyncUser
)はRealm Object Serverの中心となるクラスです。サーバーと同期されたRealmファイルを開くには、まず認証済みのユーザーでログインする必要があります。SyncUser
クラスはユーザー名+パスワードによる認証に加え、SNSを使う方法などいくつかの認証方式をサポートしています。
ユーザーの作成、およびログインは次の2つの値が必要です。
- 接続先のRealm ServerのURL
- ユーザーを一意に識別するための
SyncCredentials
オブジェクト(例: ユーザー名+パスワード、アクセストークンなど)
接続先サーバーのURLを構築する
次のように接続先のRealm Serverを示すNSURL
オブジェクトを作成します。
let serverURL = NSURL(string: "http://my.realmServer.com:9080")!
さらに詳しくは認証についてのドキュメントをご覧ください。
認証
どのような認証プロバイダがサポートされているかについては、Realm Object Serverの認証についてのドキュメントをご覧ください。
下記に示すのは標準でサポートされているさまざまな認証プロバイダを用いてSyncCredentials
オブジェクトを作成するサンプルです。
let usernameCredentials = SyncCredentials.usernamePassword(username: "username", password: "password")
let googleCredentials = SyncCredentials.google(token: "Google token")
let facebookCredentials = SyncCredentials.facebook(token: "Facebook token")
let iCloudCredentials = SyncCredentials.iCloud(token: "iCloud token")
ほとんどの認証方式では各サービス固有の認証トークンを使用します。認証トークンは各アプリケーション側で取得してください。
ユーザー名とパスワードによる認証だけは特別で、Realm Serverによって管理されます。そのため、ユーザー管理を完全にコントロールできます。認証プロセスの際に、AuthenticationActions
パラメータを用いて新たにユーザーを登録するか、既存ユーザーでログインするかを選ぶことができるのも特徴です。
ユーザーを認証する
必要なパラメータが用意できれば、Realm Object Serverに接続し、同期されたRealmファイルを開くことができます。
SyncUser.logIn(with: credentials,
server: serverURL) { user, error in
if let user = user {
// can now open a synchronized Realm with this user
} else if let error = error {
// handle error
}
}
ユーザーを活用する
Realm Mobile Platformでは1つのアプリケーション内で同時に複数のユーザーを利用することができます。例えばメールのクライアントアプリでは複数のアカウントを切り替えて使用できます。そのような動作を実現するために、複数のユーザーを好きなときに同時に有効にできます。認証済みのユーザーオブジェクトをすべて取得するにはUser.all()
クラスメソッドを利用します。
ログアウト
アプリケーションの利用者がアカウントからログアウトするには、User.logOut()
メソッドを呼びます。そうすると、サーバーに送信されていないデータをすべて送信した後に、すべてのローカルデータは削除されます。
同期されたRealmを開く
同期されたRealmを開くにはこれまでのスタンドアローンで動作するRealmを開く場合と同じように、ConfigurationオブジェクトとRealmクラスのコンストラクタを使用します。ただし、同期されたRealmを開く場合はsyncConfiguration
プロパティに認証済みのSyncUser
オブジェクトとRealm URLを設定する必要があります。
Realm URLはチルダ(~
)を含むことができ、各ユーザーのディレクトリを表します。Realm URL中のチルダは自動的にユーザーIDに置き換えられます。この仕組みは、ユーザーごとに別のRealm URLを指定する場合に非常に便利です。
同期されたRealmに対してはinMemoryIdentifier
とfileURL
を指定することはできません。同期されたRealmはキャッシュとしてディスクにファイルを持ちますが、フレームワークによって完全に隠蔽されています。
// Create the configuration
let syncServerURL = URL(string: "realm://localhost:9080/~/userRealm")!
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: syncServerURL))
// Open the remote Realm
let realm = try! Realm(configuration: config)
// Any changes made to this Realm will be synced across all devices!
ログ出力
数種類のログ出力レベルをサポートしています。下記のようにSyncManager
オブジェクトを通じて、ログレベルを調整できます。
SyncManager.shared.logLevel = .off
ログ出力レベルに関して詳しくは、Realm Object Serverの設定に関するドキュメントをご覧ください。
エラー処理
APIのほとんどはコンプリーションブロックを用いてローカルで発生したエラーを取得できます。グローバルエラーを捕捉するにはSyncManager
のエラーハンドラを使用します。
SyncManager.shared.errorHandler = { error, session in
// handle error
}
マイグレーション
同期されたRealmでは自動マイグレーションをサポートしています。ただし、スキーマを変更した場合にはこれまでと同様にスキーマバージョンを増加させる必要があります。現状では新しくクラスを追加したり、既存クラスにプロパティを追加するといった、追加によるスキーマ変更のみをサポートしています。このとき、マイグレーションブロックは呼ばれません。
コンフリクトの解決
コンフリクトの解決については、Realm Object Serverのドキュメントをご覧ください。
FAQ
Realmのライブラリは、どのくらいの容量がありますか?
アプリケーションをリリース用にビルドすると、Realmライブラリによってアプリケーション自体の容量は1MB程度増加します。
現在、配布されているRealmのライブラリは、iOSとwatchOS、tvOSのシミュレータ用のバイナリやデバッグシンボル、そしてBitcodeを含むため著しく大きくなっています。それらはビルド時にXcodeによって自動的に取り除かれます。
Realmはオープンソースソフトウェアですか?
その通りです!RealmのC++で書かれた内部ストレージエンジンと各プログラミング言語のSDKは完全にオープンソースソフトウェアとして公開されています。ライセンスはApache 2.0です。
Realmはオプションとしてデータ同期のコンポーネントをクローズドソースで提供していますが、これはRealmを組み込みのデータベースとして利用するだけなら必須ではありません。
アプリケーションを起動したときにMixpanelへの通信が発生しているのを確認しましたが、なぜでしょうか?
Realmは匿名の統計データを収集しています。統計データの送信は、デバッガに接続されているか、シミュレータ上で動作している時のみ行われます。送信するデータに個人を特定する情報は一切含まれません。データにはRealmのバージョン、(iOS、OS Xなどの)プラットフォーム、プログラミング言語、などの情報が含まれ、それはRealmの製品の向上にのみ利用されます。統計データの送信は、リリースビルド、またはデバイス上で動作している時には行われません。あくまでも、開発中にデバッガに接続している時と、シミュレータ上で動作している時だけ送信されます。実際にどのようなデータを収集しているのかは、ソースコードから確認していただけます。
トラブルシューティング
クラッシュレポート
私たちは開発者の方にクラッシュレポーターを使用していただくことを推奨しています。Realmの操作のうちの多くは(ディスクI/Oを伴う操作などと同様に)実行時に失敗する可能性があります。そのため、クラッシュレポートを収集することは、何が原因で問題が発生したのかを特定し、エラー処理の改善や不具合の修正に有効です。
多くの商用のクラッシュレポーターはログを収集するオプションを提供しています。この機能を有効にすることを強く推奨します。Realm例外が発生した時や、致命的な状態に陥った際にはメタデータの情報をログに出力しており(そこにユーザーデータは一切含まれていません)、それらのログは我々が問題を調査する際に役に立ちます。
Realmの問題や不具合を報告するには
Realmについて問題を発見した際は、GitHubで問題を報告するか、help@realm.ioにEメール、またはSlackで報告してください。その際には、こちらで問題を再現できるように、できるだけ多くの情報をあわせて教えてください。
下記に示す情報は問題を解決するために非常に役に立ちます。
- あなたが実際にやりたいこと・目的。
- 期待している結果。
- 実際に発生した結果。
- 問題の再現方法。
- 問題を再現、または理解できるサンプルコード (そのままビルドして実行できるXcodeプロジェクトだと理想的です) 。
- Realm、Xcode、OS Xのバージョン
- (利用している場合は)CarthageやCocoaPodsなどのパッケージマネージャのバージョン
- 問題の発生するプラットフォーム(iOSやtvOSなど)、OSのバージョン、アーキテクチャ(例: 64-bit iOS 8.1)。
- クラッシュログやスタックトレース。上述のクラッシュレポートの項目も参考にしてください。
Realmの再インストール(CarthageやCocoaPodsを使っている場合)
RealmをCocoaPodsまたはCarthageなどのパッケージマネージャを使ってインストールしていて、ビルドエラーが起こる場合、次の2つの原因が考えられます。 Realmが対応していないバージョンのパッケージマネージャを使用していてセットアップに失敗している、あるいはビルドツールやXcodeの古いキャッシュが残ってることが原因です。
その場合は、パッケージマネージャによって作成されたフォルダを削除してから、再インストールしてください。
また、Xcodeの Derived Data ディレクトリの削除と、 ビルドディレクトリの削除 も実行してみてください。そうすることで、ツールチェーンをアップデートしたり、ターゲットを新しく追加したことによる不整合や、複数のターゲットでライブラリを共有している場合などに起こるキャッシュの問題を解決することができます。ビルドディレクトリを削除するには、’Option’キーを押したままXcodeの’Product’メニュー>’Clean Build Folder…‘と選択します。もしくは、’Help’メニューの検索エリアに’Clean’と入力して検索結果に表示された’Clean Build Folder…‘を選択する方法もあります。
CocoaPodsを使っている場合
RealmをインストールするにはCocoaPods 0.39.0以降が必要です。
CocoaPodsを利用していて問題が起きた場合は、CocoaPodsによるライブラリとプロジェクトとの統合を解除してもう一度やり直してください。統合を解除して再インストールするには、ターミナルを起動し、下記のコマンドをプロジェクトのルートディレクトリで実行します。
pod cache clean Realm
pod cache clean RealmSwift
pod deintegrate || rm -rf Pods
pod install --verbose
rm -rf ~/Library/Developer/Xcode/DerivedData
Podsディレクトリを削除する代わりに、cocoapods-deintegrateプラグインを利用することもできます。CocoaPods 1.0ではこのプラグインは最初からCocoaPodsに組み込まれています。それ以前のバージョンのCocoaPodsを利用している場合は、gem install cocoapods-deintegrate
というコマンドでインストールすることができます。そのあとはpod deintegrate
とすると、CocoaPodsがプロジェクトに加えた変更が取り除かれます。
また、DerivedDataディレクトリの削除や、Buildディレクトリをクリーンすることも有効です。そうすることでCocoaPodsに起因する問題が解決したと非常に多く報告されています。Buildディレクトリをクリーンする方法は、’Optionキー’を押しながら、’Product’の’Clean Build Folder…‘を選択します。’Help’の検索メニューに”Clean”と入力することで表示される’Clean Build Folder…‘メニュー項目を選択するという方法もあります。
Carthageを使っている場合
RealmをインストールするにはCarthage 0.9.2以降が必要です。
Carthageによってインストールされたすべてのライブラリを削除して再インストールするには、ターミナルを起動し、下記のコマンドをプロジェクトのルートディレクトリで実行します。
rm -rf Carthage
rm -rf ~/Library/Developer/Xcode/DerivedData
carthage update
Realm Coreのダウンロードに失敗する場合
(特にCocoaPodsを使っていて)Realmがビルドされるときには、自動的にRealm Coreライブラリをスタティックライブラリとしてダウンロードし、Realm-Cocoaプロジェクトに組み込みます。 下記のようなメッセージが表示された場合は、ビルド時のCoreライブラリのダウンロードに失敗しています。
Downloading core failed. Please try again once you have an Internet connection.
このエラーが起こるのは、下記のいずれかの理由によります。
- 米国輸出規制法のリストに含まれる国のIPアドレスが割り当てられている。 米国の法律に従うために、Realmは上記のリストに含まれる国で使用することはできません。 さらに詳しいことはライセンス条項をお読みください。
- 中国国内、あるいは国レベルのファイアウォールによってCloudFlareまたはAmazon AWS S3へのアクセスを制限している国からアクセスしている。 さらに詳しいことはこちらのIssueをご覧ください。
- Amazon AWS S3が障害により一時的に利用できなくなっている。AWS Service Health Dashboardを見てサービスの稼働状況を確認し、 障害が解決された後で再度やり直してください。