RealmSwift入門 – 覚えておきたい、リレーションの定義からクエリ、マイグレーションまで –

今回のテーマはモバイルデータベースの Realm です。 この記事では基本的なCRUD以外の、実際にアプリケーションを開発していく中で必要になるリレーションの定義やマイグレーションなどを取り上げていきたいと思います。
2019.06.11

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。モバイルアプリサービス部で iOS アプリエンジニアとして働き始めた田辺です。現在研修で大阪に来ていますが研修が終わるまでに一記事書きたいと考えていたので、その目標が達成できそうで嬉しいです。

今回のテーマはモバイルデータベースの Realm です。 業務で Realm を使っていること、今までの開発ではかなり単純なデータ(参照も更新も限定的)の保存に Realm を扱っていたため、実案件に入るまでにきちんと準備をしておきたいということで、今回 Realm をテーマにした記事を書くことにしました。

Developers.IO では 2 年程前に Realm の基本的な部分を解説した記事が投稿されています。

[iOS] Realm を使ってみた 〜環境構築から CRUD まで〜

この記事ではリンク先の記事で取り上げられていないものの、実際にアプリケーションを開発していく中で必要になることを紹介していきたいと思います。

扱う内容は以下になります。

  • 導入
  • モデルの定義
  • Realm を扱う時のスレッドについて
  • マイグレーション
  • Realm browser の使い方

動作環境

  • Swift 5.0
  • Xcode 10.2.1

導入

公式ドキュメントにて Dynamic Framework, CocoaPods, Carthage を使ったインストール方法が紹介されていますが、今回は CocoaPods を使った導入を行います。

といってもリンク先の記事と手順は変わりません。プロジェクトファイル内の Podfile にpod 'RealmSwift'と記述してpod installで Realm をインストールします。

インストールが終わったら .xcworkspace を開きます。ここまでで CocoaPods での導入は終わりです。あとは Realm のオブジェクトの生成、読み書きを行うファイルでimport RealmSwiftするだけで使用できます。

モデル定義

リレーションの定義、プライマリキーなどを取り上げます。

モデル定義のサンプルコード

今回説明に使用するモデル定義の概要を説明します。ユーザーは名字と名前、年齢を持ち、本を複数持つことができます。(1 対多の関係)。また、ペットとして犬を一つ持つことができます。(1 対 1 の関係)。それぞれのモデルは作成日を持っています。以上の関係をモデル定義としてコードで表現したのが以下になります。一つずつ解説するので今の段階で理解できなくても問題ありません。

class User: Object {
    @objc dynamic var firstName = ""
    @objc dynamic var lastName =  ""
    @objc dynamic var age = 0
    @objc dynamic var createdAt = Date()
    let books = List<Book>()
    var fullName: String {
        return "\(firstName) \(lastName)"
    }
    @objc dynamic var dog: Dog?

    override static func primaryKey() -> String? {
        return "name"
    }
}

class Book: Object {
    @objc dynamic var title = ""
    @objc dynamic var synopsis: String? = nil
    @objc dynamic var createdAt = Date()
    let users = LinkingObjects(fromType: User.self, property: "books")

    override static func primaryKey() -> String? {
        return "title"
    }

}

class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var createdAt = Date()
    let users = LinkingObjects(fromType: User.self, property: "dog")
    override static func indexedProperties() -> [String] {
        return [ "name" ]
    }
}

プロパティの定義に dynamic キーワードが必要になる理由と@objc

Realm モデルのプロパティは内部で Objective-C で書かれた専用のアクセスメソッドに置き換えられるため dynamic キーワードをプロパティに付与する必要があります。

Swift4 以前は dynamic キーワードだけで良かったのですが、以降は@objc をつける必要があります。

これは Realm 側ではなく Swift の変更が原因ものです。Swift3 では Objective-C のクラスを継承している場合や、dynamic キーワードを付与している変数やメソッドがある場合に、コンパイラによる暗黙的な@objc 推論というものが行われていました。これにより Swift のコードを Objectve-C ランタイムから呼べるようになっていたのですが、Swift4 では必要な部分を除いて、暗黙的な@objc 推論が行われないようになったので、Objective-C ランタイムを使った動的ディスパッチを行うため dynamic キーワードを使いたい場合は@objc 推論が行われるよう明示的に@objc とつける必要があります。

より詳しく正確な説明を読みたい場合はプロポーザルを参照してください。

class User: Object {
    @objc dynamic var firstName = ""
    @objc dynamic var lastName =  ""
    @objc dynamic var age = 0
    @objc dynamic var createdAt = Date()
}

関連(リレーションシップ)の定義

モデル同士のつながりがある状態を定義できる関連(リレーションシップ)を設定することができます。関連は RDBMS で使用される一般的な用語です。

1 対 1 の関係

上述の通り、User は Dog を一つ持つことができます。Realm のモデル定義においてこの関係を表現する場合は Optional 型(非 Optional による定義は不可)で@objc dynamic varを使用してプロパティ定義を行います。

class User: Object {
    @objc dynamic var dog: Dog?
}

1 対多の関係

上述の通り、User は Book を 1 個以上持つことができます。この関係を表現する場合は List というクラスを使います。

class User: Object {
    let books = List<Book>()
}

逆方向の関連

逆方向の関連を簡単に取得したい時に使うのが LinkingObjects です。User は Book を 1 つ以上持っています。同じ Book を持っている User を取得したい時に逆方向の関連を使用すると Book から User の一覧を取得することができます。コードにすると以下のようになります。

class Book: Object {
    let users = LinkingObjects(fromType: User.self, property: "books")
}

プライマリキー

主キーとも言います。オブジェクトの同一判定に使用されるプロパティで、これに指定されたプロパティの値はユニークなものになります。プライマリキーを指定すると同名のプロパティを保持している場合保存されません。一意性が確保されインデックスも作成されるので効率的に検索と更新ができます。

class Book: Object {
    @objc dynamic var title = ""
    override static func primaryKey() -> String? {
        return "title"
    }
}

データベースに保存されないプロパティの定義

User モデルは名字と名前をプロパティとして持っていて、データベースにも保存されますが、フルネームを保存する必要はないかもしれません。このようなデータベースに保存されないプロパティを定義したい場合は複数の方法があります。

  • ignoredProperties()をオーバーライドする
  • getter のみの computed property の定義
  • let で定義した stored property の定義

以下のコードでそれぞれの方法でデータベースに保存されないプロパティを定義してみました。

    @objc dynamic var firstName = ""
    @objc dynamic var lastName =  ""
    @objc dynamic var tmpValue = ""
    // computed property
    var fullName: String {
        return "\(firstName) \(lastName)"
    }
    // letで定義したstored property
    let identifier = 1
    // ignoredProperty()のオーバーライド
    override static func ignoredProperties() -> [String] {
        return ["tmpValue"]
    }
}

読み込み

objectsメソッドで単一の型のオブジェクト一覧を取り出すことが出来ます。返り値としてResult<Element>インスタンスを返し、filterでのフィルタリング、sortでのソートができます。一覧を取ってくると聞くと速度が心配になってきますが、公式ドキュメントに

Data is only read when objects and properties are accessed. This allows you to represent large sets of data in a performant way. https://realm.io/docs/javascript/latest/#filtering

とある通り、Realm のクエリは遅延ロードになっているので検索した範囲内でのアクセスは高速です。また、Result インスタンスはデータの表示を行っているに過ぎないので immutable です。

全件取得

objects<Element>(_:)メソッドで取得します。

let realm = try! Realm()
realm.objects(User.self)

filter(_:_:)でのフィルタ

Realm のコレクションクラスに対してクエリからコレクションの要素を絞り込む filter(::)メソッドが用意されています。

例:age20 以上、firstName が"sigeyuki"

realm.objects(User.self).filter("age >= 20 && firstName == 'sigeyuki'")

クエリはチェーンさせることが出来るので上のコードを以下のように書くこともできます。

realm.objects(User.self)
  .filter("age >= 20")
  .filter("firsbtName == 'sigeyuki'")

クエリについて

Result、List、LinkingObjects などのコレクションはクエリから要素を絞り込むことができます。

Realm supports many common predicates https://realm.io/docs/swift/latest#filtering

many とある通り、NSPredicate に定義されている多くの構文に対応していますが、完全に対応しているわけではないようです。いくつか抜粋して今回のサンプルコードで表現してみます。

引数を使用する構文
let age = 20
let name = "sigeyuki"
var results = realm.objects(User.self).filter("age >= %@ && firstName == %@", age, name)
比較演算子と論理演算子

一般的な演算子と変わりありません。いくつか例をコードにしました。

realm.objects(User.self).filter("age >= 20") // 20以上
realm.objects(User.self).filter("age <= 20") // 20以下
realm.objects(User.self).filter("age == 20") // 20と等しい
realm.objects(User.self).filter("age > 20") // 20より大きい
realm.objects(User.self).filter("age < 20") // 20未満
realm.objects(User.self).filter("age != 20") // 20じゃない
realm.objects(User.self).filter("age BETWEEN {18, 23}") 18以上 ~ 23以下
realm.objects(User.self).filter("age >= 18 AND age <= 23") // 18以上 ~ 23以下
realm.objects(User.self).filter("age == 18 || age == 23") // 18か23
文字列の比較演算子

User の firstName で検索してみます。

// 前方一致
realm.objects(User.self).filter("firstName BEGINSWITH 'y'") // yoshiki yushi
// 部分一致
realm.objects(User.self).filter("firstName CONTAINS 'y'") // sigeyuki, ryotaro, yoshiki, kyou
// 後方一致
realm.objects(User.self).filter("firstName ENDSWITH 'ki'") // sigeyuki, yoshiki
// パターンマッチ
realm.objects(User.self).filter("firstName LIKE '*sh*'") // yushi, yoshiki

プライマリキーでの取得

realm.object(ofType:forPrimaryKey:)メソッドを使用してプライマリキーからオブジェクトを取得できます。

realm.object(ofType: Book.self, forPrimaryKey: "指輪物語")
// Optional<Book>
//   some : Book {
//  title = 指輪物語;
//  synopsis = (null);
//  createdAt = 2019-06-10 05:46:42 +0000;
}

sortedでの一覧のソート

sorted(byKeyPath:ascending:)sorted(byKeyPath:)などを使用してソートします。ソートを行わない場合は順番が必ずしも保証されません。

let over20Users realm.objects(User.self).filter("age >= 20")

// 年齢を昇順で並び替え
let sortedUsers = over20Users.sorted('age', true)

スレッドについて

Within individual threads, you can just treat everything as regular objects without worrying about concurrency or multithreading. There is no need for any locks or resource coordination to access them (even if they are simultaneously being modified on other threads) and it is only modifying operations that have to be wrapped in transactions. https://realm.io/docs/dotnet/latest/#threading

公式ドキュメントにもある通り、Realm は並列処理やマルチスレッドによるデータベースに対するロックなどを考慮する必要はありません。しかし Realm インスタンスや Object、Results などはスレッドセーフではないのでスレッドやディスパッチキューの間で共有できず、アクセスした場合は例外が生じます。

let users = realm.objects(User.self)
DispatchQueue.global().async {
  print(users) // Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread.'
}

しかし、以下のコードは動作します。

let newUser = User()
DispatchQueue.global().async {
  print(newUser)
}

このnewUserはまだ Realm データベースに保存されておらず、Realm に管理されていないオブジェクトなのでスレッドを跨いでも動作します。

ThreadSafeReference を使った別スレッド間でのオブジェクトの受け渡し

Object などのThreadConfinedプロトコルに準拠しているオブジェクトを異なるスレッド間で受け渡しするにはThreadSafeReferenceを使用します。 ThreadSafeReference インスタンスは ThreadConfined に準拠しているオブジェクトへの参照を含んでいて(RLMThreadSafeReference<RLMThreadConfined>)、スレッド内で resolve(_:)メソッドを使用して中身を取り出します。

let realm = try!(Realm(configuration: config))
let users = realm.objects(User.self)
let objectRef = ThreadSafeReference(to: users)
DispatchQueue.global().async {
  print(objectRef) // ThreadSafeReference<Results<User>>
  let globalRealm = try! Realm(configuration: config)
  guard let object = globalRealm.resolve(objectRef) else { return }
  print(object) // スレッドを跨いでも落ちない
}

マイグレーション

モデル定義を変更した際に保存されているデータを新しいモデル定義に対応させる処理のことをマイグレーションといいます。

Realm の場合ですと、モデル定義を変更した場合は古いモデル定義を新しいモデル定義へマイグレーションさせなければいけません。例えば nickName というプロパティを User のモデル定義に追加するとNSErrorがスローされマイグレーションが必要だというエラーメッセージが返ってきます。

// マイグレーションを行わなかった時のエラー出力
Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=io.realm Code=10 "Migration is required due to the following errors:
- Property 'User.nickName' has been added." UserInfo={NSLocalizedDescription=Migration is required due to the following errors:
- Property 'User.nickName' has been added., Error Code=10}

マイグレーションの設定はRealm.Configurationに設定し、Realm クラスの初期化時に渡します。実際のマイグレーション処理はRealm.ConfigurationmigrationBlockにクロージャを代入します。今回は単純なプロパティの追加でマイグレーション処理は必要ないのでmigrationBlockへのマイグレーション処理の代入は不要です。古いデータ構造から新しいデータ構造に移行する際にデータの加工が必要ない場合はRealm.Configurationのセットのみで後は Realm がよしなにやってくれます。

// Configurationを生成
var config = Realm.Configuration()
config.schemaVersion = 1
let realm = try!(Realm(configuration: config))

Realm browser の基本的な使い方

Realm browserの機能を紹介するためにストアページに英語で記載されている機能を拙訳します。

  • .realm ファイルに保存されているオブジェクト一覧の閲覧、フィルタリング
  • 特定のオブジェクトの値の更新
  • リレーションを定義したオブジェクト間の移動
  • リアルタイムに更新されるデータの確認
  • サンプルデータベースの生成

Realm ファイルの保存先

保存先はシミュレータと実機で異なります。

シミュレータの場合

LLDB かアプリケーション内のコードでRealm.Configuration.defaultConfiguration.fileURL!を出力すれば保存先の Path を取得できます。

実機の場合

Xcode のメニューからWindow > Device and Simulatorでデバッグ権限を持つアプリを選択出来るので、デバイスを選択肢INSTALLED APPSの中から該当のアプリを選択して歯車ボタン > Download Container...で.xcappd という拡張子のついたファイルがダウンロードされます。それを右クリック > パッケージの内容を表示で中身が見れるので、AppData > Documentsに、.realm ファイルがあります。手軽さからいつもシミュレータの方で.realm ファイルを探して Realm browser で見ていることが多いです。

閲覧

Realm ファイルを Realm Browser から閲覧する方法は 2 つあります。Realm ファイルをダブルクリックするか、ターミナルでopen -a "Realm Browser" Realmファイルのpathで Realm Browser が起動して Realm ファイルがあるディレクトリを開いたウインドウが表示されるのでAllowボタンをクリックすると DB の中身が見れるようになります。

すると以下のようにデータベースの中身が表示されるようになります。

レコードの任意のカラムをダブルクリックすると編集できます。リレーションを定義したカラムは以下の画像のようにリンクのような表示になっていてクリックができます。

クリックすると参照先のオブジェクトに遷移します。

データの更新はリアルタイムで反映されるので、更新の後に.realm ファイルを展開し直す必要はありません。

Realm Browser の基本的な使い方は以上になります。

まとめ

以上、Realm の基本的な CRUD 以外に知っておきたいことを書きました。Realm は他にもまだまだ機能がありますが、任意のモデルを定義して取得の仕方を理解しつつ Realm を扱う上で気にしないといけないスレッドやマイグレーションの問題を取り上げたかったのでこのような構成になりました。

また、Realm Platformというモバイルアプリとサーバー間のデータ同期を行えるプラットフォームもあるので、実際にサンプルを作ってみてどういうことが出来るのか、また出来ないのかなどをまとめてみたいと思っています。

参考・関連

https://realm.io/docs