CoreDataのCascade deleteの挙動の検証

  • 0
    いいね
  • 0
    コメント

    以前、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

    SearchConditionは、検索条件テーブル

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

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

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

    最初の画面で指定なしを選択すると
    スクリーンショット 2016-12-10 23.56.20.png

    次の画面に行って、、「パート、契約社員、その他」を選択して、決定を押すと
    スクリーンショット 2016-12-10 23.57.06.png

    変更されて
    スクリーンショット 2016-12-10 23.54.32.png

    もう一度条件を選択して変更すると
    スクリーンショット 2016-12-10 23.54.59.png

    選択した順番通りに、条件が変更される
    スクリーンショット 2016-12-10 23.54.57.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

    スクリーンショット 2016-12-10 23.48.17.png

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

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

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

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

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

    スクリーンショット 2016-12-10 19.51.16.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
            }
    
        }
    }
    
    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日アドベントカレンダーでした。