以前、CoreDataで、配列情報を持つ必要があったので、
CoreDataのCascade deleteのサンプルを1日で作って、挙動を検証してみました。
Cascade Deleteなのに、Cascade Delegateって名前間違えましたw
完全に疲れてますねw
ちなみにRealmは、Cascade Deleteがまだ、サポートされてないので、
以前、Realmを使っていた時は、自分で自作して子テーブルまで消してました。
それに対し、CoreDataは、Cascade Deleteをサポートしています。LimitもOffsetもあります。
Cascade Deleteとは、Wikipediaによると
参照される側の関係変数の組が削除された場合、参照する側の関係変数の対応するすべての組は削除される。
同様に、参照される側の関係変数の組が更新された場合、参照する側の関係変数の外部キーの値は同じ値に更新される。
なるほど、わかりにくい
ちょっと図で具体的に説明してみます。
SearchConditionは、検索条件テーブル
WorkTyleは、検索条件の働き方の複数選択を入れるテーブルです。
検索条件に、働き方を複数選択するアプリがあったとします。
この場合の働き方は、「アルバイトや、パート、正社員、契約社員、その他」を選択できるとします。
次の画面に行って、、「パート、契約社員、その他」を選択して、決定を押すと
このサンプルで、Casdate Deleteが行われているが確かめます。
CoreDataは、SQLiteに保存するので、検索条件のSearchConditionの1つのデーブルだけで、配列情報を持つことができません。
そこで、One to Manyの関係のリレーショナルなWorkStyleというテーブルを作ります。
リレーショナルな関係を作るには、Add Entityで、SearchConditionテーブルとWorkStyleテーブルを作った後に、
Editor Styleを、GUI形式にして、SearchConditionテーブルを、Controlキーを押しながら、WorkStyleに接続すると、矢印がつながります。
その後以下の図のように、テーブルのカラム名や、プロパティー名を追加したり、変えていきます。
WorkStyleテーブルが、Nullifyから、Cascade、To Oneから、To Manyに変わっていることことに注意してください。
Model.xcdatamodeldを選択して、メニューバーのEditorから、Create NSManagedObject SubClassを選択すると、
Entityに対応するNSManagedObject SubClassが自動生成されます。
自動生成するときに、フォルダと、グループを指定し忘れると、違うフォルダとグループに自動生成されるので注意してください。
後、自動生成されたファイルのクラスと、自動生成する前に同じクラス名が存在するとエラーになるので、クラス名が衝突しないように気をつけてください。
Cascade Deleteで、僕がハマったのは、
親テーブルのSearchConditionで、働き方を複数持つためのRelationshipsのworkStyleSetに、nilを入れて上書き保存して削除しても
子テーブルのWorkStyleレコードは、外部キーが消えるだけで、全部削除されないで残るということです。
自分が検証したところ、子テーブルのWorkStyleにRelationshipsのある、親テーブルのレコードを削除しないと、
子テーブルのレコードが全部削除されないということがわかりました。
そこで、以下の実装で、解決しました。
主な実装は以下の通りです。
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
}
}
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
}
}
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日アドベントカレンダーでした。