Core DataのcreateOrUpdateはマージポリシーによって変化する

# はじめに

タイトルを悩んだが適切なタイトルの表現が難しい。Core DataにはcreateOrUpdateは存在しないが、似たようなことをしようとした場合について。

まずConstraintsをidとして設定し、そのConstraintsに従ってcontext.saveを実行する場合を想定。その際にマージポリシーによって更新されたりされなかったりが決まる。


# createOrUpdate実例

これはあくまでマージポリシーによってCreateOrUpdateっぽいことをするという例です。安全にUpdateするためにはデータをidで探してきてそれがなければCreateし、あればUpdateするのが良いはずです。

## nameプロパティにセットしない場合

### マージポリシーがNSMergeByPropertyObjectTrumpMergePolicy

![スクリーンショット 2021-08-23 17.45.53のコピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16400/4df7e04d-98d2-866f-e32d-0294e87d14e9.png)

マージポリシーをNSMergeByPropertyObjectTrumpMergePolicyに切り替え、DBをクリアした結果は次の通り。

```swift
@objc(Person)
public class Person: NSManagedObject {
    @NSManaged public var id: Int32
    @NSManaged public var name: String?
}
```

```swift
func createPerson() {

    func debug(_ person: Person) {
        print("---")
        print("person.id:", person.id)
        print("person.name:", person.name ?? "nil")
    }

    let context = persistentContainer.viewContext
    context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy // このポリシーによって変わる

    do {
        let person = Person(context: context)
        person.id = 1
        person.name = "p1"

        saveContext()

        let request = NSFetchRequest<Person>(entityName: "Person")

        let entities = try! context.fetch(request)
        debug(entities.first!)
    }

    do {
        let person = Person(context: context)
        person.id = 1
//            person.name = "p1" // ここをセットしない

        saveContext()

        let request = NSFetchRequest<Person>(entityName: "Person")

        let entities = try! context.fetch(request)
        debug(entities.first!)
    }
}

// MARK: - Core Data Saving support

func saveContext () {
    let context = persistentContainer.viewContext
    if context.hasChanges {
        do {
            try context.save()
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        }
    }
}
```

出力は下記の通り。nameをセットしなければnilによって上書きされる（というか新しいオブジェクトがid: 1として上書きされる）。

```
---
person.id: 1
person.name: p1
---
person.id: 1
person.name: nil
```

### マージポリシーがNSMergeByPropertyStoreTrumpMergePolicy

マージポリシーをNSMergeByPropertyStoreTrumpMergePolicyに切り替え、DBをクリアした結果は次の通り。

```
---
person.id: 1
person.name: p1
---
person.id: 1
person.name: p1
```

おそらく、すでにこのid: 1は作成されていてマージエラーが起きている。その際、Storeを優先するためにp1を上書きしてない。

Storeを優先するためにnameが書き換えられていればすでにp1でない場合もある。

### マージポリシーがNSErrorMergePolicyの場合

マージポリシーをNSErrorMergePolicyに切り替え、DBをクリアした結果は次の通り。


```
---
person.id: 1
person.name: p1
Fatal error: Unresolved error Error Domain=NSCocoaErrorDomain Code=133021 "(null)" UserInfo={NSExceptionOmitCallstacks=true, conflictList=(
    "NSConstraintConflict (0x600000af8b80) for constraint (\n    id\n): database: 0xf09c831374aa7f8e <x-coredata://135BBA78-677D-4C61-B065-CD5DF8EB0DB5/Person/p1>, conflictedObjects: (\n    \"0xf09c831374a67f8e <x-coredata://135BBA78-677D-4C61-B065-CD5DF8EB0DB5/Person/p2>\"\n)"
)}, ["NSExceptionOmitCallstacks": 1, "conflictList": <__NSArrayM 0x6000011ef3c0>(
NSConstraintConflict (0x600000af8b80) for constraint (
    id
): database: <Person: 0x600003c84d20> (entity: Person; id: 0xf09c831374aa7f8e <x-coredata://135BBA78-677D-4C61-B065-CD5DF8EB0DB5/Person/p1>; data: {
    id = 1;
    name = p1;
}), conflictedObjects: (
    "<Person: 0x600003c9c730> (entity: Person; id: 0xf09c831374a67f8e <x-coredata://135BBA78-677D-4C61-B065-CD5DF8EB0DB5/Person/p2>; data: {\n    id = 1;\n    name = nil;\n})"
)
)
]
```

初回は作成されたが次に同じidで更新しようとするとコンフリクトが起こり、マージを失敗させる。

# おわりに


マージポリシーのデフォルトはNSErrorMergePolicyのため、比較的ミスに気づきやすいかもしれない。

# 関連

## Realmはどうなるのか

https://qiita.com/yimajo/items/4eeb2dd04d020575b13d