Core Dataのリファレンスにある使い方を知るためにMagical Recordのライブラリを読んでみる

このスライドはAkiba.swift（2018/2/23）の発表資料です

---

# どんな発表するか案が3つありました

- やはりお前らのCore Dataは間違っている。続
- 地獄を揺さぶる（ヘルシェイク）Core Data
- まだSwaggerで消耗してるの？

---

# 最初に

自分の経験上、煽ったタイトルにすると、読んだ人は何か一言物申したくなって、そのためには揚げ足を取ろうと平常心を欠いたことでも発言したくなるので、もし、煽ったタイトルで広まることを目的としても、煽りタイトルはおすすめできません。

---

## もっと多くの人に広めたいという気持ちは分かる

私も趣味のMacアプリは作りはなるべくリファレンスをみないでやっていたりますが、そういうふうに間違った使い方をしている人って大抵動けば良くて公式リファレンスを読まずにやっていません。

そういう人にも広まるようにタイトルを工夫して煽ってしまう気持ちわかります。

---

## ちゃんとしたフィードバックがほしいなら

---

### タイトルで煽ってはいけない。さらに本文で煽ったらもっとダメ。

---

# 本題

---

## 話すこと

- Core Dataの使い方を知るには、リファレンスだけ読んでやっていくのは厳しい
- Magical RecordというCore Dataラッパーライブラリのコードを読むと理解が深まる
- しかしMagical Recordは使わないほうがいいよ

---

## Core Data のリファレンスがきびしい

- [Appleのリファレンス](https://developer.apple.com/jp/documentation/Cocoa/Conceptual/CoreData/index.html)にCore Dataに関するいろいろなTipsが日本語で書かれています
    - このリファレンス結構読むのがしんどいです
        - Core Dataは多機能なので専門用語が多い
        - Core Dataは[2006年くらいにはあったらしく](https://en.wikipedia.org/wiki/Core_Data)そのためモダンなインターフェースではない
    - パフォーマンスのための並列処理についても言及されている

---

### Core Dataの難問その一、「NSManagedObjectContext の概念がきびしい」

---

- 世の中にあるWebアプリケーションには `NSManagedObjectContext` とそのまま同じ概念はないのでピンとこない
- クライアントサイドのアプリケーションでも、`NSManagedObjectContext` のような概念は隠蔽されがち

---

## しかし、`NSManagedObjectContext` が出来る事が何かを知ると理解しやすい

- 永続化のデータをメモリ上でコントロールできる
    - ロールバックできる
    - undoやredoができる（使ったことないですが）

---

- 多量のデータの書き込みの必要があっても、データ書き込み用の `NSManagedObjectContext` を作れる
    - メインスレッドに処理させずにユーザは快適に使える
- 読み取り用の `NSManagedObjectContext` を作れる

---

## この便利な `NSManagedObjectContext` をMagical Recordがどのように作るかを知るともっとよく理解できます

---

### 用語について

- `NSManagedObjectContext` はcontext
- Magical Recordは MR と書いていきます

---

## MRはどうやってcontextを作って使ってる？

- 全てのcontextの親になるrootContextを作る
- rootContextから読み取り用のdefaultContextをつくって基本読み取りはそれを使う
- rootContextもdefaultContextもシングルトンで呼び出す
- 書き込みには書き込み用のcontextをrootContextから作成する

---

context親子の図

- 親: rootContext
    - 子: 読み込み用 defaultContext
    - 子: 書き込み用 context

---

## 読み取り用のdefaultContextとは何か

---

### defaultContextの初期化メソッド

```objc
+ (void) MR_initializeDefaultContextWithCoordinator:(NSPersistentStoreCoordinator *)coordinator;
{
    NSAssert(coordinator, @"Provided coordinator cannot be nil!");
    if (MagicalRecordDefaultContext == nil)
    {
        NSManagedObjectContext *rootContext = [self MR_contextWithStoreCoordinator:coordinator];
        [self MR_setRootSavingContext:rootContext];

        NSManagedObjectContext *defaultContext = [self MR_newMainQueueContext];
        [self MR_setDefaultContext:defaultContext];

        [defaultContext setParentContext:rootContext];
    }
}
```

- まずrootContextを作って登録 `MR_setRootSavingContext`
- defaultContextをメインキューで使うようにして作る `MR_newMainQueueContext`
- 作成したcontextをdefaultContextとしてシングルトンで呼び出せる登録 `MR_setDefaultContext`
- defaultContextの親をrootContの親をにする `setParentContext`

---

### defaultContextをメインキューで使うようにして作る `MR_newMainQueueContext`

```objc
+ (NSManagedObjectContext *) MR_newMainQueueContext
{
    NSManagedObjectContext *context = [[self alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    MRLogInfo(@"Created new main queue context: %@", context);
    return context;
}
```

NSMainQueueConcurrencyType というのでメインキュー用になる

NSMainQueueConcurrencyTypeはAppleのドキュメントから引用すると

> NSMainQueueConcurrencyTypeはアプリケーションインターフェイスでの使用に特化したもので、アプリケーションのメインキューでのみ使うことができます。 

---

### よくわからんけど

- アプリケーションインターフェイスでの使用に特化したもの
    - つまりメインスレッド用
- メインキューでのみ使う
    - まぁつまりメインスレッド用

---

### 自分がドキュメント書くなら

> NSMainQueueConcurrencyTypeはアプリケーションインターフェイスでの使用に特化しているため、アプリケーションのメインキューで利用されることを想定しています。

つまり、別スレッドで使わないでくれということ。

---

### 別スレッドで使うなとはどういうこと？

---


### Appleのドキュメントから

プライベートキューによる並列処理の実現

> ユーザに関係しないメインキューでデータ処理を実行することは、通常は避けてください。データ処理ではCPUリソースが大量に消費される可能性があり、メインキューで実行した場合は、ユーザインターフェイスが無反応になることもあり得ます。データをJSONからCore Dataにインポートするなど、アプリケーションでデータを処理する場合は、プライベートキューコンテキストを生成し、プライベートコンテキストでインポートを実行します。

---

### 多量のデータをCore Dataに保存するときメインスレッドでやると固まるから気をつけてくれよな!

---

- 多量のデータ？
    - JSONでもなんでも、件数が固定ではないものは多量になりうると思っといたほうがいい
- ユーザはどんな操作をしているか分からない
    - 動画撮ってるかもしれないし動画見てるかもしれないし
    - そのときに同じメインスレッドで多量のデータをCore Dataに保存すると操作不能になる
        - 再現性が低くて大変

---

### 保存用にプライベートキューのためのcontextを作る


MRではどうやって書き込み用のcontextを作っているか

```objc
+ (NSManagedObjectContext *) MR_newContext
{
    return [self MR_context];
}

+ (NSManagedObjectContext *) MR_context
{
    return [self MR_contextWithParent:[self MR_rootSavingContext]];
}
```

`MR_contextWithParent`はすでに説明したとおりrootContextを親にして、新しいcontextを`NSPrivateQueueConcurrencyType`で作成するだけ。すごくシンプルだ。

---


## ここまでのまとめ

- マルチスレッドで書き込み用のContextは `rootSavingContext` を親にする
  - `NSPrivateQueueConcurrencyType` で書き込み用のcontextを作成する
  - `defaultContext` とは同じ親をもつ兄弟
- 何かを表示したい時、そのcontextは `defaultContext`

---

## なぜ親子関係にするの？ Core Dataの基本について

- 子のcontextでsaveしたらそれを他のcontextにも反映したいから

例えば、

- コンテンツの一覧にお気に入りボタンがある
- 詳細画面でお気に入りボタンを押したらボタンの状態を一覧のボタンにも反映したい

---

### 親子関係で保存を伝えるとは？

- 書き込み時のcontextをsaveして親もsaveするとrootContextに変更が伝わって永続化される
- 親のcontextの変更は、子のcontextに反映される
    - NSFetchedResultsControllerを使っていれば差分検出されdelegateのメソッドが動作

context親子の図

- 親: rootContext
    - 子: 読み込み用 defaultContext
        - （[順番2] `NSFetchedResultsController`で変更が検知できる）
    - 子: 書き込み用 context 
        - （[順番1] このcontextで書き込んだら、context.parentでsaveすればrootに結果が伝わる）

---

## ここで疑問。なぜ`defaultContext`をrootにしないんだろう？

- おそらく、書き込み用の`context`をsaveして親をsaveする処理自体は結構重い
- おそらく、`defaultContext`の子を作って保存して`defaultContext`をsaveするのはメインスレッドの処理が多くなるのでは

---

## `defaultContext`の変更（話の優先度低い）

下記のメソッドで`defaultContext`を変更できる。

```objc
[NSManagedObjectContext MR_setDefaultContext:myNewContext];
```

しかし、変更できても`NSMainQueueConcurrencyType`なcontextを使うことが最も安全だし普通。MRのドキュメントでもrecommedは`NSMainQueueConcurrencyType`なので、defaultを変更してカスタマイズなcontextを利用することはあまり無いと思う。

> NOTE: It is highly recommended that the default context is created and set on the main thread using a managed object context with a concurrency type of NSMainQueueConcurrencyType.

---

## 書き込みcontextを使ってどういうふうな使い方をするの？

---

### MRにはバックグラウンドスレッドで保存処理を行うメソッドが用意されている。

基本的にはデータの作成、保存は全てバックグラウンドスレッドでやれば良くて、特定のメソッドを使うようにすればいい。

```objc
Person *person = ...;

[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){

  Person *localPerson = [person MR_inContext:localContext];
  localPerson.firstName = @"John";
  localPerson.lastName = @"Appleseed";

}];
```

- `localContext`は保存用に作成される
- MR_inContextで`localContext`から作成
- クロージャを終了するとときにはsaveされている

---

### バックグラウンド保存のコードはどうなってる？

```objc
+ (void) saveWithBlock:(void(^)(NSManagedObjectContext *localContext))block completion:(MRSaveCompletionHandler)completion;
{
    NSManagedObjectContext *savingContext  = [NSManagedObjectContext MR_rootSavingContext];
    NSManagedObjectContext *localContext = [NSManagedObjectContext MR_contextWithParent:savingContext];

    [localContext performBlock:^{
        [localContext MR_setWorkingName:NSStringFromSelector(_cmd)];

        if (block) {
            block(localContext);
        }

        [localContext MR_saveWithOptions:MRSaveParentContexts completion:completion];
    }];
}
```

- `rootSavingContext`から新しい`localContext`を作成
- 引数として渡された`block`は、`localContext performBlock:`に囲まれている

---

### ここで気になるのは次の3つ

- performBlockメソッドとは？
  - プライベートキューで作成されたContextで保存する際に利用する
- `MR_setWorkingName`とは？
- `MR_saveWithOptions:`とは？

---

### `MR_setWorkingName`について見ていく

さっきのコードでは引数`workingName` に対して`NSStringFromSelector(_cmd)`でメソッド名を渡していて、実装は次のようになっている。


```objc
- (void)MR_setWorkingName:(NSString *)workingName
{
    void (^setWorkingName)() = ^{
        [[self userInfo] setObject:workingName forKey:MagicalRecordContextWorkingName];
    };

    if (self.concurrencyType == NSMainQueueConcurrencyType && [NSThread isMainThread])
    {
        setWorkingName();
    }
    else
    {
        [self performBlockAndWait:setWorkingName];
    }
}
```

単に、userInfoにKey=MagicalRecordContextWorkingNameの ValueとしてStringを登録している。userInfoはNSMutableDictoinaryなので単に実行したメソッドを保持しているだけだろう。

---

### なにそれ

[Appleのリファレンス](https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext/1506740-userinfo)によると、`The user infomation for the context`ということで、デバッグ用だろう。

---

### 次に `MR_saveWithOptions`

このメソッドはかなり長いので、先に引数 `MRSaveParentContexts`について読んでおく。MRのOptionsだ。

```objc
typedef NS_OPTIONS(NSUInteger, MRSaveOptions) {
    /** No options — used for cleanliness only */
    MRSaveOptionNone = 0,

    /** When saving, continue saving parent contexts until the changes are present in the persistent store */
    // Google翻訳: 保存するときは、変更が永続ストアに存在するまで親コンテキストを保存し続けます。
    MRSaveParentContexts = 1 << 1,

    /** Perform saves synchronously, blocking execution on the current thread until the save is complete */
    // Google翻訳: 同期を実行して、保存が完了するまで現在のスレッドで実行をブロックする
    MRSaveSynchronously = 1 << 2,

    /** Perform saves synchronously, blocking execution on the current thread until the save is complete; however, saves root context asynchronously */
    // Google翻訳: 同期を実行し、保存が完了するまで現在のスレッドで実行をブロックします。ただし、ルートコンテキストを非同期で保存します
    MRSaveSynchronouslyExceptRootContext = 1 << 3
};
```

---

### `MR_saveWithOptions`のユースケースとそれに対応するMRのメソッド

- MRSaveParentContextsを指定
  - 非同期実行して親もsave
  - `MR_saveToPersistentStoreWithCompletion`
- MRSaveSynchronously
  - 同期実行するので終わるまで待つ
  - `MR_saveOnlySelfAndWait`
- MRSaveParentContexts | MRSaveSynchronously
  - バックグラウンドで同期実行して終わるまで待つ
  - `- (void) MR_saveToPersistentStoreAndWait;`
- MRSaveSynchronouslyExceptRootContext
  - バックグラウンドスレッドで同期実行。
  - rootContextはそれとは別スレッドの非同期実行

基本的にはMRSaveParentContext | MRSaveSynchronouslyで終わったらクロージャが動作するのが良さそう。

---

### 次に実際の`MR_saveWithOptions`メソッドをコードの中にコメントを書きながら理解していく

---

```objc
// 
- (void) MR_saveWithOptions:(MRSaveOptions)saveOptions completion:(MRSaveCompletionHandler)completion;
{
    __block BOOL hasChanges = NO;
    // NSConfinementConcurrencyTypeじゃないので次のifは通らない
    if ([self concurrencyType] == NSConfinementConcurrencyType)
    {
        hasChanges = [self hasChanges];
    }
    else
    {   // contextのperformBlockAndWaitを動作させてhasChangesがあるかどうかを取得（なぜ？）
        [self performBlockAndWait:^{
            hasChanges = [self hasChanges];
        }];
    }

    if (!hasChanges)
    {   // 何も変更無さそうならLog出力して
        MRLogVerbose(@"NO CHANGES IN ** %@ ** CONTEXT - NOT SAVING", [self MR_workingName]);
        // 完了用のクロージャをメインスレッドで実行する
        if (completion)
        {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(NO, nil);
            });
        }

        return;
    }
    // 引数から動作を決める処理。Optionなので値を取り出す必要がある
    // 次のshouldSaveParentContextsはtrueになる
    BOOL shouldSaveParentContexts = ((saveOptions & MRSaveParentContexts) == MRSaveParentContexts);
    // 次のshouldSaveSynchronouslyはfalse
    BOOL shouldSaveSynchronously = ((saveOptions & MRSaveSynchronously) == MRSaveSynchronously);
    // 次もfalse
    BOOL shouldSaveSynchronouslyExceptRoot = ((saveOptions & MRSaveSynchronouslyExceptRootContext) == MRSaveSynchronouslyExceptRootContext);
    // saveSynchronouslyは同期実行するかどうかで、falseになる
    // optionが MRSaveSynchronouslyでMRSaveSynchronouslyExceptRootContextでない、
    // もしくは
    // optionが MRSaveSynchronouslyExceptRootContext で、このcontextがrootではない
    BOOL saveSynchronously = (shouldSaveSynchronously && !shouldSaveSynchronouslyExceptRoot) ||
                             (shouldSaveSynchronouslyExceptRoot && (self != [[self class] MR_rootSavingContext]));

    id saveBlock = ^{
        MRLogInfo(@"→ Saving %@", [self MR_description]);
        MRLogVerbose(@"→ Save Parents? %@", shouldSaveParentContexts ? @"YES" : @"NO");
        MRLogVerbose(@"→ Save Synchronously? %@", saveSynchronously ? @"YES" : @"NO");

        BOOL saveResult = NO;
        NSError *error = nil;

        @try
        {   // まず保存
            saveResult = [self save:&error];
        }
        @catch(NSException *exception)
        {
            MRLogError(@"Unable to perform save: %@", (id)[exception userInfo] ?: (id)[exception reason]);
        }
        @finally
        {
            [MagicalRecord handleErrors:error];
            // 保存完了して、引数的にshouldSaveParentContextsもtrue、かつ親もいるならifに入る
            if (saveResult && shouldSaveParentContexts && [self parentContext])
            {
                // Add/remove the synchronous save option from the mask if necessary
                MRSaveOptions modifiedOptions = saveOptions;

                if (saveSynchronously)
                {   // 同期実行しないのでここは通らない
                    modifiedOptions |= MRSaveSynchronously;
                }
                else
                {   // 非同期実行するから、optionからMRSaveSynchronouslyを消している
                    modifiedOptions &= ~MRSaveSynchronously;
                }
                // 親でsaveするため再帰処理
                // If we're saving parent contexts, do so
                [[self parentContext] MR_saveWithOptions:modifiedOptions completion:completion];
            }
            else
            {   // 保存完了しなかったり、親で保存しなかったり、親がなかったりしたら終了（自分がroot）
                if (saveResult)
                {
                    MRLogVerbose(@"→ Finished saving: %@", [self MR_description]);
                }

                if (completion)
                {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        completion(saveResult, error);
                    });
                }
            }
        }
    };

    if (saveSynchronously)
    {
        [self performBlockAndWait:saveBlock];
    }
    else
    {
        [self performBlock:saveBlock];
    }
}
```

---

ポイントは

- 再帰処理で親contextをたどり、`rootContext`までsaveすれば終了
- hasChangesで変更をチェックしているが、`performBlockAndWait`でチェックする理由は知らん
- `NSConfinementConcurrencyType` は非推奨になった何かなので無視

---

オプションにより同期処理もサポートした汎用的な書き込みメソッドのため手厚いが、やってることはそれほど大したことじゃないことがわかったと思う。

---

# Magical Recordは使ったほうが良い？

- [GitHubのリポジトリ見ると保守されていないと感じる](https://github.com/magicalpanda/MagicalRecord)
- 上のことをふまえたらSwiftで自分でやっていくほうがいい]

---

# まとめ

- 電影少女2018はいいぞ
- ヴァイオレットエヴァーガーデンもいいぞ