CoreDataのCascade deleteの挙動の検証

以前、CoreDataで、配列情報を持つ必要があったので、

CoreDataのCascade deleteのサンプルを1日で作って、挙動を検証してみました。

https://github.com/applideveloper/CascadeDeleteSample

Cascade Deleteなのに、Cascade Delegateって名前間違えましたw

完全に疲れてますねw

ちなみにRealmは、Cascade Deleteがまだ、サポートされてないので、

以前、Realmを使っていた時は、自分で自作して子テーブルまで消してました。

それに対し、CoreDataは、Cascade Deleteをサポートしています。LimitもOffsetもあります。

https://github.com/realm/realm-java/issues/1104

Cascade Deleteとは、Wikipediaによると

https://ja.wikipedia.org/wiki/%E5%A4%96%E9%83%A8%E3%82%AD%E3%83%BC#CASCADE

参照される側の関係変数の組が削除された場合、参照する側の関係変数の対応するすべての組は削除される。
同様に、参照される側の関係変数の組が更新された場合、参照する側の関係変数の外部キーの値は同じ値に更新される。

なるほど、わかりにくい

ちょっと図で具体的に説明してみます。

![スクリーンショット 2016-12-10 23.36.16.png](https://qiita-image-store.s3.amazonaws.com/0/25434/3fa5e773-bd92-137e-ba59-eb2c0b8cf12b.png)

SearchConditionは、検索条件テーブル

WorkTyleは、検索条件の働き方の複数選択を入れるテーブルです。

検索条件に、働き方を複数選択するアプリがあったとします。

この場合の働き方は、「アルバイトや、パート、正社員、契約社員、その他」を選択できるとします。

最初の画面で指定なしを選択すると
![スクリーンショット 2016-12-10 23.56.20.png](https://qiita-image-store.s3.amazonaws.com/0/25434/d964dc71-0540-fbb4-a315-77743b801993.png)

次の画面に行って、、「パート、契約社員、その他」を選択して、決定を押すと
![スクリーンショット 2016-12-10 23.57.06.png](https://qiita-image-store.s3.amazonaws.com/0/25434/6cf9df00-60e3-00d5-4be6-c1594d446035.png)

変更されて
![スクリーンショット 2016-12-10 23.54.32.png](https://qiita-image-store.s3.amazonaws.com/0/25434/0d9395b2-710e-b451-bc87-2e4633681992.png)

もう一度条件を選択して変更すると
![スクリーンショット 2016-12-10 23.54.59.png](https://qiita-image-store.s3.amazonaws.com/0/25434/5323009d-82d6-b7a2-d06f-583e956df0eb.png)

選択した順番通りに、条件が変更される
![スクリーンショット 2016-12-10 23.54.57.png](https://qiita-image-store.s3.amazonaws.com/0/25434/69acaf5a-a84b-e254-199d-8449676e9a29.png)

このサンプルで、Casdate Deleteが行われているが確かめます。

CoreDataは、SQLiteに保存するので、検索条件のSearchConditionの1つのデーブルだけで、配列情報を持つことができません。

そこで、One to Manyの関係のリレーショナルなWorkStyleというテーブルを作ります。

リレーショナルな関係を作るには、Add Entityで、SearchConditionテーブルとWorkStyleテーブルを作った後に、

Editor Styleを、GUI形式にして、SearchConditionテーブルを、Controlキーを押しながら、WorkStyleに接続すると、矢印がつながります。

その後以下の図のように、テーブルのカラム名や、プロパティー名を追加したり、変えていきます。

![スクリーンショット 2016-12-10 23.46.59.png](https://qiita-image-store.s3.amazonaws.com/0/25434/de1b23e2-3132-d75e-d473-721170097c8d.png)

![スクリーンショット 2016-12-10 23.48.17.png](https://qiita-image-store.s3.amazonaws.com/0/25434/4d04b5d9-d4d9-53a7-f11e-0f2e0d2c5779.png)

WorkStyleテーブルが、Nullifyから、Cascade、To Oneから、To Manyに変わっていることことに注意してください。

Model.xcdatamodeldを選択して、メニューバーのEditorから、Create NSManagedObject SubClassを選択すると、

Entityに対応するNSManagedObject SubClassが自動生成されます。

自動生成するときに、フォルダと、グループを指定し忘れると、違うフォルダとグループに自動生成されるので注意してください。

後、自動生成されたファイルのクラスと、自動生成する前に同じクラス名が存在するとエラーになるので、クラス名が衝突しないように気をつけてください。

<img width="373" alt="スクリーンショット 2016-12-10 19.51.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/25434/7e8a1dcc-8da4-d9e3-2ada-d7fba2162b01.png">


Cascade Deleteで、僕がハマったのは、

親テーブルのSearchConditionで、働き方を複数持つためのRelationshipsのworkStyleSetに、nilを入れて上書き保存して削除しても

子テーブルのWorkStyleレコードは、外部キーが消えるだけで、全部削除されないで残るということです。

自分が検証したところ、子テーブルのWorkStyleにRelationshipsのある、親テーブルのレコードを削除しないと、

子テーブルのレコードが全部削除されないということがわかりました。

そこで、以下の実装で、解決しました。

https://github.com/applideveloper/CascadeDelegateSample

主な実装は以下の通りです。

``` SearchConditionViewController.swift
import UIKit

class SearchConditionViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    
    var searchConditionArray = [SearchCondition]()

    override func viewDidLoad() {
        super.viewDidLoad()

        self.navigationItem.title = "条件選択"
        
        // UITableViewDelegate
        self.tableView.delegate = self
        // UITableViewDataSource
        self.tableView.dataSource = self
        
        self.tableView.register(
            UINib(
                nibName : SelectWorkStyleTableViewCell.className,
                bundle : nil
            ),
            forCellReuseIdentifier: SelectWorkStyleTableViewCell.className
        )
        
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        searchConditionArray = SearchConditionManager.sharedManager.fetchAll() ?? [SearchCondition]()
        
        self.tableView.reloadData()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

extension SearchConditionViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if let tableViewCell = self.tableView.cellForRow(at: indexPath),
           tableViewCell.className == SelectWorkStyleTableViewCell.className {
            
            let workStyleStoryboard = UIStoryboard(name: Constraints.WorkStyleStoryboard.rawValue, bundle: nil)
            
            guard let selectWorkStyleViewController = workStyleStoryboard.instantiateInitialViewController() as? SelectWorkStyleViewController else {
                // send error report
                return
            }
            
            if searchConditionArray.count > 0 {
                let searchCondition = self.searchConditionArray[indexPath.row]
                
                if let workStyleSet = searchCondition.workStyleSet,
                    let workStyleArray = workStyleSet.array as? [WorkStyle] {
                    
                    var selectedWorkStyleArray = [String]()
                    
                    for workStyle in workStyleArray {
                        selectedWorkStyleArray.append(workStyle.workStyleType ?? "")
                    }

                    selectWorkStyleViewController.selectedWorkStyleArray = selectedWorkStyleArray
                    
                }
            }
            
            self.navigationController?.pushViewController(selectWorkStyleViewController, animated: true)
        }
    }
}

extension SearchConditionViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if searchConditionArray.count == 0 {
            return 1
        } else {
            return searchConditionArray.count
        }
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        guard let selectWorkStyleTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: SelectWorkStyleTableViewCell.className) as? SelectWorkStyleTableViewCell else {
            return ViewUtlity.getTableViewCell(color: UIColor.clear)
        }
        
        if searchConditionArray.count == 0 {
            selectWorkStyleTableViewCell.workStyleLabel.text = "指定なし"
        } else {
            let searchCondition = self.searchConditionArray[indexPath.row]
            
            if let workStyleSet = searchCondition.workStyleSet,
                let workStyleArray = workStyleSet.array as? [WorkStyle] {
                var workStyleString = ""
                
                for (index, workStyle) in workStyleArray.enumerated() {
                    if let workStyleType = workStyle.workStyleType,
                        workStyleType.isEmpty == false {
                        if index == 0 {
                            workStyleString += workStyleType
                        } else {
                            workStyleString += "," + workStyleType
                        }
                    }
                }
                
                print(workStyleString)
                
                if workStyleString.isEmpty == true {
                    selectWorkStyleTableViewCell.workStyleLabel.text = "指定なし"
                } else {
                    selectWorkStyleTableViewCell.workStyleLabel.text = workStyleString
                }
                
            }
        }
        
        return selectWorkStyleTableViewCell
    }
}
```

``` SelectWorkStyleViewController.swift
import UIKit

class SelectWorkStyleViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    
    var workStyleTypeArray = [String]()
    
    var selectedWorkStyleArray = [String]()

    override func viewDidLoad() {
        super.viewDidLoad()

        workStyleTypeArray.append(WorkStyleType.Arbeit.rawValue)
        workStyleTypeArray.append(WorkStyleType.Part.rawValue)
        workStyleTypeArray.append(WorkStyleType.RegularEmployee.rawValue)
        workStyleTypeArray.append(WorkStyleType.ContractEmployee.rawValue)
        workStyleTypeArray.append(WorkStyleType.TemporaryStaff.rawValue)
        workStyleTypeArray.append(WorkStyleType.Other.rawValue)
        
        // UITableViewDelegate
        self.tableView.delegate = self
        // UITableViewDataSource
        self.tableView.dataSource = self
        
        self.tableView.register(
            UINib(nibName : WorkStyleTableViewCell.className, bundle : nil),
            forCellReuseIdentifier: WorkStyleTableViewCell.className
        )
        
        self.tableView.reloadData()
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    
    @IBAction func tapDesideButton(_ sender: Any) {
        if let searchCondition = SearchConditionManager.sharedManager.create() {
            var workStyleArray = [WorkStyle]()
            
            for workStyleType in self.workStyleTypeArray {
                if self.selectedWorkStyleArray.contains(workStyleType) {
                    if let workStyle = WorkStyleManager.sharedManager.create() {
                        workStyle.workStyleType = workStyleType
                        workStyleArray.append(workStyle)
                    }
                }
            }
            
            searchCondition.workStyleSet = NSOrderedSet(array: workStyleArray)
            
            SearchConditionManager.sharedManager.update(updateSearchCondition: searchCondition)
        }
        
        let _ = self.navigationController?.popViewController(animated: true)
    }
}

extension SelectWorkStyleViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let tableViewCell = self.tableView.cellForRow(at: indexPath)
    
        if let workStyleTableViewCell = tableViewCell as? WorkStyleTableViewCell  {
            if workStyleTableViewCell.isWorkStyleTableViewCellSelected {
                // すでに選択されていた時
                workStyleTableViewCell.isWorkStyleTableViewCellSelected = false
            } else {
                // 未選択の時
                workStyleTableViewCell.isWorkStyleTableViewCellSelected = true
            }
            
            if let workStyleLabelText = workStyleTableViewCell.workStyleLabel.text {
                if selectedWorkStyleArray.contains(workStyleLabelText) {
                    // すでに選択されていた時
                    let newSelectedWorkStyleArray = selectedWorkStyleArray.filter({ (workStyle: String) -> Bool in
                        if workStyle == workStyleLabelText {
                            return false
                        } else {
                            return true
                        }
                   })
                    
                    self.selectedWorkStyleArray = newSelectedWorkStyleArray
                } else {
                    self.selectedWorkStyleArray.append(workStyleLabelText)
                }
            }
        }
        
        self.tableView.reloadData()
    }
}

extension SelectWorkStyleViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.workStyleTypeArray.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let workStyleTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: WorkStyleTableViewCell.className) as? WorkStyleTableViewCell else {
            return ViewUtlity.getTableViewCell(color: UIColor.clear)
        }
        
        let workStyleType = workStyleTypeArray[indexPath.row]
        
        if self.selectedWorkStyleArray.contains(workStyleType) {
            workStyleTableViewCell.isWorkStyleTableViewCellSelected = true
        } else {
            workStyleTableViewCell.isWorkStyleTableViewCellSelected = false
        }
        
        workStyleTableViewCell.workStyleLabel.text = workStyleType
        
        return workStyleTableViewCell
    }
}
```

``` SearchConditionManager.swift
import UIKit
import CoreData

class SearchConditionManager: NSObject {
    
    static let sharedManager = SearchConditionManager()
    
    private override init() {
        
    }
    
    func create() -> SearchCondition? {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            // FIXME: send error report
            return nil
        }
        
        let viewContext = appDelegate.persistentContainer.viewContext

        guard let searchConditionEntity = NSEntityDescription.entity(forEntityName: SearchCondition.className, in: viewContext) else {
            // FIXME: send error report
            return nil
        }
        
        guard let searchCondition = NSManagedObject(entity: searchConditionEntity, insertInto: viewContext) as? SearchCondition else {
            return nil
        }
        
        do {
            try viewContext.save()
            return searchCondition
        } catch let error as NSError {
            // FIXME: send error report
            print("error.description = \(error.description)")
            return nil
        }
    }

    func fetchAll() -> [SearchCondition]? {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            // FIXME: send error report
            return nil
        }
        
        let viewContext = appDelegate.persistentContainer.viewContext
        
        let fetchRequest: NSFetchRequest<SearchCondition> = SearchCondition.fetchRequest()
        
        //ascendind:true 昇順、false 降順です
        let sortDescripter = NSSortDescriptor(key: "updateTime", ascending: false)
        fetchRequest.sortDescriptors = [sortDescripter]
        
        do {
            return try viewContext.fetch(fetchRequest)
            
        } catch let error as NSError {
            // FIXME: send error report
            print("error.description = \(error.description)")
            return nil
        }
    }
    
    func update(updateSearchCondition: SearchCondition) {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            // FIXME: send error report
            return
        }
        
        let viewContext = appDelegate.persistentContainer.viewContext
        
        if let searchCondition = self.create() {
            searchCondition.workStyleSet = updateSearchCondition.workStyleSet
            searchCondition.updateTime = NSDate()
        }
        
        do {
            try viewContext.save()
            
            
            if let searchConditionArray = self.fetchAll(),
                searchConditionArray.count >= 2 {
                
                print("start")
                for searchCondition in searchConditionArray {
                    if let workStyleSet = searchCondition.workStyleSet,
                        let workStyleArray = workStyleSet.array as? [WorkStyle] {
                        var workStyleString = ""
                        
                        for (index, workStyle) in workStyleArray.enumerated() {
                            if let workStyleType = workStyle.workStyleType,
                               workStyleType.isEmpty == false {
                                if index == 0 {
                                    workStyleString += workStyleType
                                } else {
                                    workStyleString += "," + workStyleType
                                }
                            }
                        }
                        
                        if workStyleString.isEmpty == true {
                            workStyleString = "指定なし"
                        }
                        print(workStyleString)
                    }
                }
                print("end")
                
                
                for (index, searchCondition) in searchConditionArray.enumerated() {
                    if index == 0 {
                        continue
                    }
                    
                    self.delete(searchCondition: searchCondition)
                }
            }
            
        } catch let error as NSError {
            // FIXME: send error report
            print("error.description = \(error.description)")
        }
    }
    
    func delete(searchCondition: SearchCondition){
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            // FIXME: send error report
            return
        }
        
        let viewContext = appDelegate.persistentContainer.viewContext
       
        viewContext.delete(searchCondition)
        
        do {

            try viewContext.save()
        } catch let error as NSError {
            // FIXME: send error report
            print("error.description = \(error.description)")
            return
        }
        
    }
}
```

```swift
import UIKit
import CoreData

class WorkStyleManager: NSObject {
    static let sharedManager = WorkStyleManager()
    
    private override init() {
        
    }
    
    func create() -> WorkStyle? {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            // FIXME: send error report
            return nil
        }
        
        let viewContext = appDelegate.persistentContainer.viewContext
        
        guard let workStyleEntity = NSEntityDescription.entity(forEntityName: WorkStyle.className, in: viewContext) else {
            // FIXME: send error report
            return nil
        }
        
        guard let workStyle = NSManagedObject(entity: workStyleEntity, insertInto: viewContext) as? WorkStyle else {
            return nil
        }
        
        do {
            try viewContext.save()
            return workStyle
        } catch let error as NSError {
            // FIXME: send error report
            print("error.description = \(error.description)")
            return nil
        }
    }
}
```

自分のCasdate Deleteの実装は、更新前の親レコード取得して、書き換えて、更新ではなく、新規作成して、保存します。
そして、保存が成功したら、古い親レコードを削除するという実装です。

他にこういうやり方があるよ、実装間違ってるよっていうところがあれば、ぜひ教えてください。

以上、遅くなりましたが、2016年12日10日アドベントカレンダーでした。