NSManagedObjectの永続化タイミングとスレッド間での同期


CoreDataについて理解が浅すぎてイマイチよくわからないことが多いので的を絞って調査してみた。

# 疑問

* NSManagedObjectはいつ永続化されるのか
* 子コンテキストで行った更新を親コンテキスト管理下のオブジェクトと同期出来るのか（別スレッドのオブジェクト更新を他のスレッドに伝えたい）

# NSManagedObjectはいつ永続化されるのか

Persistent StoreにSQLiteを使用して[SQLを出力するように設定](http://qiita.com/key/items/ef9a0ec34aa6e63ebca9)し、空のオブジェクトを生成して永続化するタイミングを調査した。

```objectivec
// step 1
EntityObject *entityObject = (EntityObject *) [NSEntityDescription insertNewObjectForEntityForName:@"Entity"
                                                                            inManagedObjectContext:self.managedObjectContext];

// step 2
NSError *error = nil;
[self.managedObjectContext save:&error];
```

step 1のタイミングでデータベースに接続してメタデータを取得していることが解る。このタイミングでは（当然ながら）データの更新処理、すなわち *CREATE* , *UPDATE* , *DELETE* は行われていない。

```text
2014-07-07 20:07:01.551 CoreDataTest[21062:60b] CoreData: annotation: Connecting to sqlite database file at "/Users/key/Library/Application Support/iPhone Simulator/7.1/Applications/E8F1CAD4-5E36-4FEE-8CAC-30838625FAC6/Documents/HogeProj.sqlite"
2014-07-07 20:07:01.552 CoreDataTest[21062:60b] CoreData: sql: SELECT TBL_NAME FROM SQLITE_MASTER WHERE TBL_NAME = 'Z_METADATA'
2014-07-07 20:07:01.553 CoreDataTest[21062:60b] CoreData: sql: pragma journal_mode=wal
2014-07-07 20:07:01.553 CoreDataTest[21062:60b] CoreData: sql: pragma cache_size=200
2014-07-07 20:07:01.554 CoreDataTest[21062:60b] CoreData: sql: SELECT Z_VERSION, Z_UUID, Z_PLIST FROM Z_METADATA
```

step 2で明示的に保存処理を行っている。この辞典で同期的にSQLiteへの永続化処理が実施された（INSERT文）。

```text
2014-07-07 20:07:26.641 CoreDataTest[21062:60b] CoreData: sql: BEGIN EXCLUSIVE
2014-07-07 20:07:26.642 CoreDataTest[21062:60b] CoreData: sql: SELECT Z_MAX FROM Z_PRIMARYKEY WHERE Z_ENT = ?
2014-07-07 20:07:26.642 CoreDataTest[21062:60b] CoreData: sql: UPDATE Z_PRIMARYKEY SET Z_MAX = ? WHERE Z_ENT = ? AND Z_MAX = ?
2014-07-07 20:07:26.642 CoreDataTest[21062:60b] CoreData: sql: COMMIT
2014-07-07 20:07:26.643 CoreDataTest[21062:60b] CoreData: sql: BEGIN EXCLUSIVE
2014-07-07 20:07:26.643 CoreDataTest[21062:60b] CoreData: sql: INSERT INTO ZENTITY(Z_PK, Z_ENT, Z_OPT, ZNAME, ZTITLE) VALUES(?, ?, ?, ?, ?)
2014-07-07 20:07:26.644 CoreDataTest[21062:60b] CoreData: sql: COMMIT
2014-07-07 20:07:26.644 CoreDataTest[21062:60b] CoreData: sql: pragma page_count
2014-07-07 20:07:26.644 CoreDataTest[21062:60b] CoreData: annotation: sql execution time: 0.0003s
2014-07-07 20:07:26.645 CoreDataTest[21062:60b] CoreData: sql: pragma freelist_count
2014-07-07 20:07:26.645 CoreDataTest[21062:60b] CoreData: annotation: sql execution time: 0.0003s
```


## 子コンテキストで行った更新を親コンテキスト管理下のオブジェクトと同期出来るのか

子コンテキストで行ったNSManagedObjectへの変更は親コンテキストの管理するオブジェクトと同期されるのか確認してみる（`self.managedObjectContext`などは適宜読み替えてください）。

```objectivec
// step 1 entityObjectを生成する
self.entityObject = (EntityObject *) [NSEntityDescription insertNewObjectForEntityForName:@"Entity"
                                                                   inManagedObjectContext:self.managedObjectContext];
NSLog(@"entity object: %@", weakSelf.entityObject);

__weak typeof(self) weakSelf = self;

// 別スレッドでオブジェクトを更新
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
    // スレッド内で子コンテキストの生成
    NSManagedObjectContext *localContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    localContext.parentContext = weakSelf.managedObjectContext;

    // objectIDを元にEntityObjectを再取得
    EntityObject *localEntityObject = (EntityObject *) [localContext objectWithID:weakSelf.entityObject.objectID];
    localEntityObject.name = @"name";
    localEntityObject.title = @"title";

    // step 2
    NSLog(@"local entity object: %@", localEntityObject);
    NSLog(@"before save: %@", weakSelf.entityObject);

    // step 3
    NSError *localError;
    [localContext save:&localError];

    NSLog(@"after save: %@", weakSelf.entityObject);
});
```


step 1の`self.entityObject`は生成したばかりなのでプロパティは空っぽ。

```
2014-07-08 11:52:43.245 CoreDataTest[32428:60b] entity object: <EntityObject: 0x926eee0> (entity: Entity; id: 0x926ef10 <x-coredata:///Entity/tC8FA11C7-A73F-463C-8C5D-E18E64716C932> ; data: {
    name = nil;
    title = nil;
})
```


step 2は別スレッドで実行されており、`self.entityObject` のobjectIDを元に `localEntityObject` を取得して、どのように更新されるかを確認する。

`localEntityObject`は前行で処理している値が代入されている。

```
2014-07-08 11:53:09.141 CoreDataTest[32428:60b] local entity object: <EntityObject: 0x8e70840> (entity: Entity; id: 0x926ef10 <x-coredata:///Entity/tC8FA11C7-A73F-463C-8C5D-E18E64716C932> ; data: {
    name = name;
    title = title;
})
```

`self.entityObject`は空のまま。

```
2014-07-08 11:53:09.798 CoreDataTest[32428:60b] before save: <EntityObject: 0x926eee0> (entity: Entity; id: 0x926ef10 <x-coredata:///Entity/tC8FA11C7-A73F-463C-8C5D-E18E64716C932> ; data: {
    name = nil;
    title = nil;
})
```


step 3で`NSManagedObjectContext:save:`を実行すると、`self.entityObject`の値が更新される。 **なお、このタイミングでは永続化処理が行われない（INSERT, UPDATE文が流れない）。**

```
2014-07-08 11:53:32.064 CoreDataTest[32428:60b] after save: <EntityObject: 0x926eee0> (entity: Entity; id: 0x926ef10 <x-coredata:///Entity/tC8FA11C7-A73F-463C-8C5D-E18E64716C932> ; data: {
    name = name;
    title = title;
})
```


# まとめ

* `NSManagedObjectContext:save:`を呼んだタイミングで永続化される
* 子となるNSManagedObjectContextで`NSManagedObjectContext:save:`を実行しても、永続化ストアに保存されない
* 子となるNSManagedObjectContextで`NSManagedObjectContext:save:`を実行すると、親のNSManagedObjectContextが管理するNSManagedObjectの値が同期する

# 感想

子となるNSManagedObjectContextで保存しても永続化されないのは新発見（考えてみれば別スレッドで行われた更新が永続化されないのは当然な気がするけど、SQLクエリレベルで追いかけたら理解が深まった）。

`NSManagedObjectContext:refreshObject:mergeChanges:`がどういう動作をするのかまだ分かっていない（公式のドキュメントを仔細に読めって話かもですが）。

MagicalRecordを使用した場合、調査したような挙動をしないのでこちらは個別に調査しようと思います。

----

参考

* [[Objective-C] CoreDataを使う](http://qiita.com/edo_m18/items/8bc7615eb2f6d546705d)