[iOS] RealmでスタンドアローンのRLMObjectを活用する

Realm Advent Calendar 15日目を担当する[@yusuga_](https://twitter.com/yusuga_)です。

Realmは[2014年10月末くらい](https://github.com/yusuga/YSRealmStore/commit/ad28b2d70ab8e643e9e5c13264959c611dbda0c9)からいじり始めて[あーだこーだ悩みながら](http://qiita.com/yusuga/items/2a2cbf13080e1bed642f)もRealmの採用を決め、[Realm meetup #1](http://www.slideshare.net/yuuusugawara/realm-45831571)でお話しさせていただいたりなんかもして、なんだかんだ1年以上使ってきました。最終的には[Aplos](http://aplos.picospec.co.jp
)というTwitterクライアントでの採用を決め、メインのツイート関連から細かい設定情報まで全てRealmを使用しています。

Realmで一番苦しめられたのは、例外によるクラッシュです。Realmの設計思想として間違った操作に対するエラーは基本的には全て例外が投げられます。
導入初期の例外は[ドキュメント](https://realm.io/jp/docs/objc/latest/)も日本語で用意されていますし、例外自体にも何が原因かが明記されているので比較的簡単に対処できると思います。

問題はアプリを実際に稼働するようになった後に稀に発生する例外です。私の経験で一番多かったのは`RLMObject`(Realmのモデルクラス)に関する例外です。これがなかなか厄介で例外の内容を見ただけでは何が原因か判断つきづらいことが多かったです。

そこで今回は実際にアプリで運用していく中で一番多かった`RLMObject`の例外を減らすために、スタンドアローンの`RLMObject`の活用方法について紹介します。

*※ この記事は Realm v0.96.3 を元に書いています。*

# 前提知識 - RLMObjectの状態

`RLMObject`には2種類の状態があります。

	1. スタンドアローンのRLMObject
	2. Realm(データベース)に紐づけられているRLMObject

スタンドアローンとは保存前のRLMObjectの事で`-[RLMObject realm]`が`nil`になっています。 (※ Realmのドキュメント中で`standalone objects`と[記載されている](https://github.com/realm/realm-cocoa/blob/master/Realm/RLMObject.h#L214)のでこの記事ではこのような状態を`スタンドアローン`または`スタンドアローンオブジェクト`という呼び方にしています。)

この2つの大きな違いは永続化されているかどうかはもちろんですが、スタンドアローンだと通常のオブジェクトと同様にプロパティを変更できますが、データベースに保存後の`RLMObject`はトランザクション内でのみ変更が可能となります。

```objectivec:RLMObjectの生成、永続化、変更の流れ
// オブジェクトを作成する
Person *author = [[Person alloc] init];

// スタンドアローンなので変更可
author.name    = @"David Foster Wallace";

// オブジェクトをRealmに追加する
[realm beginWriteTransaction];
[realm addObject:author]; // Realmに保存されました。
[realm commitWriteTransaction];

// Realmに保存済みなのでプロパティの変更は不可。以下はクラッシュします。
author.name = @"other name"; 

// Realmに保存後にプロパティを変更する場合はトランザクション内で行います。
[realm beginWriteTransaction];
author.name = @"other name"; 
[realm commitWriteTransaction];

```

CoreDataだとオブジェクトの生成には必ずコンテキスト(NSManagedObjectContext)が必要だったのに対して、Realmでは`init`しただけでは通常のオブジェクトとして扱え、永続化したければRealmに追加すればいいだけでモデルオブジェクトが非常に扱いやすくなっています。

# Realmに紐づけられたRLMObjectをスタンドアローンに戻す

本題です！
Realmに紐づけられた`RLMObject`には制約があります。

	1. プロパティの変更はトランザクションが必要
	2. -[RLMObject isInvalidated] == YES だとアクセス不可(例外が発生)

前述の通りスタンドアローンだと通常のオブジェクトと同様に扱えるのでRealmの制約が一切なくなります。永続化はしたいけど実際に扱うときには通常のオブジェクトと同様に扱いたいという場合にはスタンドアローンに戻してしまえばいいのです。

## Copyでスタンドアローンに戻す

`RLMObject`をコピーします！ …あれ`RLMObject`って`copy`できたっけ？と思いますよね。そうです、残念ながら現在(Realm v0.96.3)`RLMObject`は`NSCopying`に準拠していません。コピーの[対応予定](https://twitter.com/k_katsumi/status/618795030988238849)はあるそうですが、おそらく用途から考えるとrealmとの紐づけを維持したままコピーという可能性が高いですかね。

そこで[Realm-JSON/RLMObject+Copying](https://github.com/matthewcheok/Realm-JSON/blob/master/Realm%2BJSON/RLMObject%2BCopying.h)を使います。`-[RLMObject deepCopy]`でRealmの紐づけをなくして同等のRLMObjectを生成することができます。
`deepCopy`した`RLMObject`はスタンドアローンになっているので以下のように扱うことができます。

```objectivec:スタンドアローンのRLMObjectを作成、変更
// オブジェクトを作成する
Person *author = [[Person alloc] init];
author.name    = @"David Foster Wallace";

// オブジェクトをRealmに追加する
[realm beginWriteTransaction];
[realm addObject:author]; // これでRealmに保存されました。
[realm commitWriteTransaction];

// 保存したauthorをコピー
Person *copiedAuthor = [author deepcopy];

XCTAssertNil(copiedAuthor.realm); // true
XCTAssertEqualObjects(author.name, copiedAuthor); // true

// トランザクションがなくても変更可能
copiedAuthor = @"other name";
```

# スタンドアローンオブジェクトを使いこなす

さて、永続化したオブジェクトをわざわざコストかけてコピーするなんて **本末転倒じゃん!** という声も聞こえてきそうですが、 実際に運用するにあたってRealmの制約はボディブローのようにじわじわきいてきて、原因が補足しづらいのRealmの例外に苦しむことになります。(※ Realmの仕様をきちんと把握していれば当然防げますがはまりやすい点が多いかなと思います)

`RLMObject`の運用ルールは設けるとして、もっと手軽に`RLMObject`を扱えばそういった事故も少なくなり、Realmのメリットを享受しつつデメリットを減らすということができます。

それでは次にスタンドアローンオブジェクトを利用することのメリットをあげてみます！

## スタンドアローンのメリット1 - 自由な変更

(何回も書いてますが)スタンドアローンだと通常のObjective-Cのオブジェクトとして扱えるのでトランザクションなしで変更が行えます。
ちょっとした変更などではメリットを感じられないと思いますが、以下のようなケースだと非常に有用です。

[Aplos](http://aplos.picospec.co.jp)で実際に使用している画面を例に説明します。[Aplos](http://aplos.picospec.co.jp)の外観には`TWAppearance`という`primaryKey`を持つ`RLMObject`を使用しています。外観を変更する場合は以下の流れになっています。

	1. 画面上部がプレビュー、下部が外観の変更になります。
	2. 最初に現在の外観を元にプレビューします。
	3. 様々な変更が行われ、それが逐一プレビューに反映されます。
	4. 最後に「保存」か「キャンセル(1つ前の画面に戻る)」が行われて編集が終わります。

![IMG_0664.PNG](https://qiita-image-store.s3.amazonaws.com/0/16700/68fa1282-3d71-5cbb-d6be-00ad7c0e86de.png "IMG_0664.PNG")

### Aplosで採用している実装

`TWAppearance`を`deepCopy`したものを元に変更を行っています。そして最終的に「保存」なら上書き更新し、「キャンセル」なら`TWAppearance`を破棄するという流れになっています。

```objectviec:AppearanceViewController
- (void)setAppearance:(TWAppearance *)appearance
{
	_appearance = [appearance deepCopy];
} 

- (void)saveButtonClicked
{
	[self.realm transactionWithBlock:^{
        [self.realm addOrUpdateObject:self.appearance];
    }];
}
```


### 素直にRealmを使用する実装方法

変更中に常にトランザクションを開くという実装が考えられます。
※ Realmの[Slack](http://slack.realm.io/)で[岸川さん](https://twitter.com/k_katsumi)が紹介していました。

```objectviec:AppearanceViewController
- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    [self.realm beginWriteTransaction];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    
    if (self.isCancelled) {
        [self.realm cancelWriteTransaction];
    } else {
        [self.realm commitWriteTransaction];
    }
}
```

このように編集画面が表示された時にトランザクションを開始して、編集画面を閉じる時(ボタンを押した時とか他のところでもいいですが)にトランザクションをコミットするかキャンセルするかで変更をデータベースに更新させるかというのを判断する形です。これはRealmの特徴で **すべての書き込み処理は読み込み処理をブロックしない** というのがあるのでトランザクションを開きっぱなしにするという選択が選びやすいです。

#### トランザクションを開きっぱなしにする場合の注意点

- 当然ながらトランザクションの閉じ忘れに十分注意しなければなりません。RLMRealmは二重でトランザクションを開こうとすると[例外が発生](https://github.com/realm/realm-cocoa/blob/master/Realm/ObjectStore/shared_realm.cpp#L284)します。[-[RLMRealm inWriteTransaction]](https://github.com/realm/realm-cocoa/blob/master/Realm/RLMRealm.h#L113)を使用すればすでにトランザクションが開かれているかの確認はできますが、トランザクションを開くたびに確認するような設計だと他の問題が生じそうですよね…。

### ここでdeepCopyを採用した理由

変更画面の遷移などが今後複雑化した時を考えるとトランザクションの閉じ忘れに注意するよりかは、`TWAppearance`を保存するか破棄するかのみに気をつける方が手軽に扱えると思います。編集中の`TWAppearance`が別画面または別スレッドで意図しない更新がされることも防げます。また`TWAppearance`を1つだけ`deepCopy`する程度ならローコストでコスト上の問題がないというのも採用理由の一つです。

# スタンドアローンのメリット2 - isInvalidatedでのクラッシュを防ぐ

`RLMObjet`は削除されると`Invalidated`になりアクセスが不可(例外)になります。

```objectivec:
@interface ViewController : UIViewController

@property (nonatomic) Tweet *tweet; // TweetはRLMObjectです

@end
```

例えばこのように`tweet`を保持してこの画面を構成している場合に、別画面や別スレッド等でこの`tweet`が削除された場合を考慮しなければなりません。

## Notificationで確認

一番素直なのは通知を受けることです。

```objectivec:
// Realmの通知を監視するように登録します
self.token = [realm addNotificationBlock:^(NSString *note, RLMRealm * realm) {
    [myViewController updateUI];
}];
```

データベースが変更された際に保持している`tweet`が`isInvalidated`になっていないかを確認しそれに応じてUIを更新します。

## deepCopyする

`deepCopy`してスタンドアローンに戻せば、これらの心配は無くなります。ただし`tweet`自体がデータベースによる更新がなくなるので注意が必要ですが、逆に`tweet`自体の変更が必要ない場合に有用です。
結局は利用する場面によると思うのですが、一時的な画面であったり、変更を受け取りたくなかったり、簡易的に扱いたいなどそういった場合にスタンドアローンオブジェクトを使うという選択ができると思います。

# ~~スタンドアローンのメリット3 - スレッドチェックによる例外を防ぐ~~

## 悲しい事実が発覚

さて、次は私が一番メリットに感じていて、実際にかなりのクラッシュを減らすことができたメリット3へと続く予定だったのが途中で悲しい事実が発覚しました。
メリット3を要約すると`RLMObject`は`dealloc`時にスレッドチェックが行われるので`RLMObject`を直接保持(propertyとかで)しているとクラッシュする場合があるので直接保持したい場合はdeepCopyした方が安全で、もしもdeepCopyしたくないなら`RLMResults`で保持しようという内容でした。

そしてある程度このセクションの記事を書いた後、realm-cocoaの該当コードへのリンクを貼ろうといろいろ確認していたところ`v0.92.0`でこのdeallocでのスレッドチェックが[追加](https://github.com/realm/realm-cocoa/commit/8a49cab247de5005e328cf486fe85fbf13d3f1cf)されていたのですが、`v0.92.3`で[削除](https://github.com/realm/realm-cocoa/commit/e2ecb0207698c3e4e7e79a2f92981d012bc6078f)されました・・・。

[It should be safe to deallocate an RLMObject from any thread #1893](https://github.com/realm/realm-cocoa/issues/1893)で話し合われている通り、マルチスレッドのblocksが`RLMObject`をretainした場合に別スレッドで`RLMObject`が解放されてしまうので、このdealloc時のスレッドチェックはなかなか辛かったです。最終的にはRealmのコア側に修正が入ってこの問題は解決したようです。

アプリでは、当時たまたま`v0.92.0`を使ったため起きたということが今頃判明して少し落ち込んでいます。(今は`0.95.3`を使っていますが、いまだにdealloc時のスレッドチェックの仕様に気をつけながら設計していました…。)

## 以下はv0.92.0〜v0.92.3でのみ起こったことです…書いちゃったので残しておきます。

Realmに紐づけられた`RLMObject`は別スレッドに渡すことができません。(Rleam v0.96.3の時点)
これはRLMRealmは[スレッド毎にキャッシュ](https://github.com/realm/realm-cocoa/blob/master/Realm/RLMRealmUtil.mm#L41)、管理されており、`RLMObject`にアクセスする際には紐づけられている`realm`とアクセスしているスレッドが同一かが[確認](https://github.com/realm/realm-cocoa/blob/master/Realm/ObjectStore/shared_realm.cpp#L264)されるためです。

```objectivec:スレッドが違うためクラッシュするアクセス方法
Tweet *tweet = [Tweet objectForPrimaryKey:tweetID];
    
dispatch_async(queue, ^{
    NSLog(@"%@", tweet); // 例外
});

```

これもスタンドアローンオブジェクトなら問題なく行えます。

```objectivec:スタンドアローンなら別スレッドでもアクセス可能
Tweet *tweet = [Tweet objectForPrimaryKey:tweetID];
tweet = [tweet deepCopy];
    
dispatch_async(queue, ^{
    NSLog(@"%@", tweet);
});
```

これで通常のマルチスレッドでオブジェクトの扱い方に気をつければいいだけになります。

さて、通常はこんな使い方が活躍する場面は少ないと思いますが、これが一番生きるのは`RLMObject`を直接保持する場合です。

```objectivec:
@interface ViewController : UITableViewController

@property (nonatomic) Tweet *tweet; // TweetはRLMObjectです

@end
```

され、これは一見問題ないように見えますが、落とし穴が1つあります。 **`-[NSObject dealloc]`の呼び出しは必ずしもメインスレッドではない** ということです。
この例だと`tweet`は表示のためにメインスレッドで取り扱うので`realm`はメインスレッドと紐づいています。さて`dealloc`がメインスレッド以外で呼び出されたらどうでしょう。別スレッドで`tweet`にアクセスしているので例外が発生してしまいます。dealloc時のスレッドチェックは0.92.0で加わったので[随分苦しみました](https://twitter.com/k_katsumi/status/595945375803969536)。

# RLMObject, RLMResultsの運用ルール

まず原則として`RLMObject`または`RLMResults`を保持している場合は必ずデータベースの変更通知を受け取るということを守る必要があります。

```objectivec:
// Realmの通知を監視するように登録します
self.token = [realm addNotificationBlock:^(NSString *note, RLMRealm * realm) {
    [myViewController updateUI];
}];
```

## 通知を受け取った時のRLMObjectの対応

UIの更新に加えて、`-[RLMObject isInvalidated]`を必ず確認する必要があります。データが更新された場合は問題ないのですが、削除された場合には`-[RLMObject isInvalidated] == YES`になりアクセスした時点で **例外が発生** します。`RLMObject`を直接保持している場合にはこの点に注意する必要があります。

## 通知を受け取った時のRLMResultsの対応

UIの更新を確実に行う必要があります。`RLMResults`はクエリ条件に応じて保持している結果が自動で更新されます(CoreDataの`NSFetchedResultsController`のモデル版みたいな挙動です)。
自動で結果が更新されるのでもし削除された場合には、データベースの変更通知を受け取った時点でUIを更新しないと`-[RLMResults objectAtIndex:]`などで範囲外エラーを起こす場合があります。

## 結論: RLMObjectを直接保持しない方がよいのかもしれない

`-[RLMObject isInvalidated]`との確認って煩雑だし、もしも忘れた場合には即例外のクラッシュが起きるのでとても危険です。
そこで私は直接`RLMObject`を保持するのは避けています。もし直接保持したい場合は、`deppCopy`したスタンドアローンオブジェクトのみにしています(実際にはこれに加えて変更を受け取りたくないなど諸々の理由がある場合にdeepCopyしている)。

もしも単体の`RLMObject`が必要な場面では`RLMResults`を保持してそれに対してクエリを使って取得します。

```objectivec:RLMObjectは直接保持しない設計
@interface TweetViewController ()

@property (nonatomic) int64_t tweetID; // この画面で使用するツイートのID
@property (nonatomic) RLMResults *tweets;

@end

@implementation TweetViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.tweets = [Tweet objectsWhere:@"id = %lld", self.tweetID];
}

- (RLMObject *)tweet
{
    return [[self.tweets objectsWhere:@"id = %lld", self.tweetID] firstObject];
}

@end
```

ツイートにアクセスする場合には`-[self tweet]`を使用します(※ Realmのフェッチは非常に高速なのでこの例でのコスト面の問題は全くないです)。`self.tweets`には常に最新の状態が反映されるので、もしも`tweetID`のツイートが削除された場合には`self.tweets`は空になるので`-[self tweet]`は`nil`になります。
これなら`-[RLMObject isInvalidated]`を気にする必要がなくなり、データベースの更新通知を受け取った場合には、UIの更新にのみ気をつければよくなります。

# まとめ

`RLMObject`のコンテキストを使わずにデータベースのモデルクラスとして扱えるというのはRealmのトップクラスのメリットだと感じています。`RLMObject`をスタンドアローンに戻すのに最初は`RLMObject`から`NSDictionary`を作成して再度`-[RLMObject initWithValue:]`する方法を検討していました。ただそれなら`RLMObject`をそのままコピーした方がシンプルだしコスト面でも優位なので`deepCopy`を使い始めました。

場面によってはスタンドアローンの`RLMObject`はとても有用で手軽に`RLMObject`を扱うことができるようになります！ぜひ活用してみてください！