【Swift】Core Dataの使い方。異なる管理オブジェクトコンテキストで保存したデータをリレーションシップに設定する。(Swift 2.1、XCode 7.2)
複数の管理オブジェクトコンテキストの注意点
前回記事で、複数の管理オブジェクトコンテキスト(以下、コンテキスト)を使って保存タイミングをエンティティごとに変える実装方法を説明した。⇒「前回記事」
1つ注意点がある。コンテキストに格納されているオブジェクトは、他のコンテキストに格納されているオブジェクトを直接参照できないことだ。
例えば、下図のように生徒オブジェクトと部活オブジェクトが参照し合う間柄であるとき、生徒と部活で異なるコンテキストを使うと、フェッチしたオブジェクトを直接別のオブジェクトのリレーションシップに設定できなくなる。⇒「リレーションシップとは」
具体例を挙げると、以下のコードのように生徒エンティティに部活クラスのリレーションシップが定義されているとする。このリレーションシップに別のコンテキストでフェッチした部活オブジェクトを設定しようとすると、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// // Student+CoreDataProperties.swift // import Foundation import CoreData extension Student { @NSManaged var name: String? //名前 @NSManaged var club: Club? //部活 } |
以下のエラーが発生する。異なるコンテキストのオブジェクトをリレーションシップに設定しようとしたためのエラーだ。
別コンテキストのオブジェクトをリレーションシップに設定する
別コンテキストのオブジェクトをリレーションシップに設定するには、下図のように、別コンテキストで保存されたデータを自分のコンテキストにフェッチしてリレーションシップに設定する。
上記の流れを実際に試してみよう。
検証に使用したXcodeプロジェクトをGitHubに置いたので、試してみる人はご利用されたし。
⇒「テスト用プロジェクト」
生徒登録用と部活登録用の画面があり、下表はボタンの機能の説明。
生徒登録
ボタン | 説明 |
---|---|
「コンテキストに追加」ボタン | 名前と所属部を設定したオブジェクトをコンテキストに追加する。同じ名前のオブジェクトがすでに存在する場合は、そのオブジェクトを編集する。 |
「保存」ボタン | コンテキストに追加したオブジェクトを外部ファイルに保存する。 |
「生徒一覧表示」ボタン | すべての生徒オブジェクトをフェッチして内容をデバッグエリアに一覧表示する。 |
部活動登録
ボタン | 説明 |
---|---|
「コンテキストに追加」ボタン | 部名と顧問を設定したオブジェクトをコンテキストに追加する。同じオブジェクトがすでに存在する場合は、そのオブジェクトを編集する。 |
「保存」ボタン | コンテキストに追加したオブジェクトを外部ファイルに保存する。 |
「部活一覧表示」ボタン | すべての部活オブジェクトをフェッチして、内容をデバッグエリアに一覧表示する。 |
実際のプレイ動画
以下は検証プロジェクトのViewController.swiftのソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
// // ViewController.swift // import UIKit import CoreData class ViewController: UIViewController, UITextFieldDelegate { //管理オブジェクトコンテキスト var contextClub:NSManagedObjectContext! var contextStudent:NSManagedObjectContext! //テキストフィールド @IBOutlet weak var clubTextField: UITextField! @IBOutlet weak var teacherTextField: UITextField! @IBOutlet weak var studentTextField: UITextField! @IBOutlet weak var activityTextField: UITextField! //最初からあるメソッド override func viewDidLoad() { super.viewDidLoad() do { //部活動用と生徒用の管理オブジェクトコンテキストを取得する。 let applicationDelegate = UIApplication.sharedApplication().delegate as! AppDelegate contextClub = applicationDelegate.contextClub contextStudent = applicationDelegate.contextStudent //Studentオブジェクトをフェッチしてラベルに設定する。 let fetchStudent = NSFetchRequest(entityName: "Student") let studentList = try contextStudent.executeFetchRequest(fetchStudent) as! [Student] for student in studentList { studentTextField.text = student.name if (student.club != nil) { activityTextField.text = student.club!.clubName clubTextField.text = student.club!.clubName teacherTextField.text = student.club!.teacher } } //デリゲート先に自分を設定する。 clubTextField.delegate = self teacherTextField.delegate = self studentTextField.delegate = self activityTextField.delegate = self } catch { print(error) } } //Returnキー押下時の呼び出しメソッド func textFieldShouldReturn(textField: UITextField) -> Bool { //キーボードをしまう。 clubTextField.endEditing(true) teacherTextField.endEditing(true) studentTextField.endEditing(true) activityTextField.endEditing(true) return true } //生徒の追加ボタン押下時の呼び出しメソッド @IBAction func pushAddStudent(sender: UIButton) { do { //生徒用オブジェクトをフェッチする。 let fetchStudent = NSFetchRequest(entityName: "Student") fetchStudent.predicate = NSPredicate(format:"name = %@", studentTextField.text!) let studentList = try contextStudent.executeFetchRequest(fetchStudent) as! [Student] let student:Student if(studentList.count == 0) { //フェッチできなかった場合は生徒用の管理オブジェクトコンテキストに新規追加する。 student = NSEntityDescription.insertNewObjectForEntityForName("Student", inManagedObjectContext: contextStudent) as! Student } else { //フェッチできた場合はそのオブジェクトを編集する。 student = studentList[0] } //生徒オブジェクトの属性値を変更する。 student.name = studentTextField.text! //部活オブジェクトを生徒用の管理オブジェクトコンテキストにフェッチする。 let fetchRequest = NSFetchRequest(entityName: "Club") fetchRequest.predicate = NSPredicate(format:"clubName = %@", activityTextField.text!) let clubList = try contextStudent.executeFetchRequest(fetchRequest) as! [Club] if(clubList.count == 0) { //フェッチできなかった場合、部活オブジェクトを作ってリレーションシップに設定する。 let club = NSEntityDescription.insertNewObjectForEntityForName("Club", inManagedObjectContext: contextStudent) as! Club club.clubName = "所属無し" club.teacher = "所属無し" student.club = club } else { //部活オブジェクトがフェッチできた場合、リレーションシップに設定する。 student.club = clubList[0] } } catch { print(error) } } //生徒の保存ボタン押下時の呼び出しメソッド @IBAction func pushStudentButton(sender: UIButton) { do { //管理オブジェクトコンテキストの中身を保存する。 try contextStudent.save() } catch { print(error) } } //部活動の追加ボタン押下時の呼び出しメソッド @IBAction func pushAddClub(sender: UIButton) { do { //部活オブジェクトをフェッチする。 let fetchClub = NSFetchRequest(entityName: "Club") fetchClub.predicate = NSPredicate(format:"clubName = %@", clubTextField.text!) let studentList = try contextClub.executeFetchRequest(fetchClub) as! [Club] let club:Club if(studentList.count == 0) { //フェッチできなかった場合は部活用の管理オブジェクトコンテキストに新規追加する。 club = NSEntityDescription.insertNewObjectForEntityForName("Club", inManagedObjectContext: contextClub) as! Club } else { //フェッチできた場合はそのオブジェクトを編集する。 club = studentList[0] } //属性値を変更する。 club.clubName = clubTextField.text club.teacher = teacherTextField.text } catch { print(error) } } //部活動の保存ボタン押下時の呼び出しメソッド @IBAction func pushClubButton(sender: UIButton) { do { //管理オブジェクトコンテキストの中身を保存する。 try contextClub.save() } catch { print(error) } } //生徒一覧表示メソッド @IBAction func displayStudent(sender: UIButton) { //生徒オブジェクトをフェッチする。 let fetchRequest2 = NSFetchRequest(entityName: "Student") let studentList = try! contextStudent.executeFetchRequest(fetchRequest2) as! [Student] print("生徒総数 \(studentList.count)") for aaa in studentList { print("=====================") print("名前 \(aaa.name!)") if let clubData = aaa.club { print("所属部 \(clubData.clubName!)") print("顧問 \(clubData.teacher!)") } else { print("所属部無し") } } print("\n\n") } //部活一覧表示メソッド @IBAction func displayClub(sender: UIButton) { //部活オブジェクトをフェッチする。 let fetchRequest = NSFetchRequest(entityName: "Club") let clubList = try! contextClub.executeFetchRequest(fetchRequest) as! [Club] print("部活総数 \(clubList.count)") for data in clubList { print("=====================") print("部活名 \(data.clubName!)") print("顧問 \(data.teacher!)") } print("\n\n") } } |
まずは、部活オブジェクトを生徒オブジェクトのリレーションシップに設定する流れを試してみる。以下はその流れ。
- 野球部を入力し「コンテキストに追加」ボタンを押す。(下図①)
- 部活登録の「保存」ボタンを押す(②③)
- 部活登録の「部活一覧表示」ボタンを押して、保存結果を確認する
- 生徒の名前と所属部を入力し「コンテキストに追加」ボタンを押す(④⑤)
- 生徒の「保存」ボタンを押す
- 「生徒一覧表示」ボタンを押して、リレーションシップの正常登録を確認する
以下は実際のプレイ動画。保存した野球部の情報が、生徒オブジェクトのリレーションシップに正常に設定、保存された。
親子のコンテキストを利用する
上記の作りには1つ欠点がある。それは、リレーションシップに設定したオブジェクトの属性値を変更しても、リレーションシップから参照する値は変わらないことだ。
例えば、生徒オブジェクトのリレーションシップに「野球部、佐野」のオブジェクトを設定したあとに、部活設定画面で顧問を「佐野」から「鬼瓦」に変更したとする。
下図のように、「保存」ボタンを押すことで外部ファイルへの保存は正常に行われるが、生徒オブジェクトのリレーションシップを見ると「佐野」のまま変わらない。
なぜなら、先ほどフェッチされた野球部のオブジェクトは生徒用のコンテキストに存在しているので、参照するときにそのオブジェクトが利用されるからである。
以下は実際のプレイ動画
アプリを再起動すれば、メモリがクリアされて外部ファイルから最新の値が取得されるのでリレーションシップのオブジェクトも「鬼瓦」になるが、アプリ起動中もリレーションシップの値は最新の値でないと困る。
そんなときに利用されるのが親子のコンテキストである。
親子のコンテキストとは、2つのコンテキストに親と子の関係を持たせ、子コンテキストの保存メソッドを呼び出すと、子コンテキストのオブジェクトが親コンテキストにマージされる機能である。
検証用プロジェクトのコンテキストを親子のコンテキストに変えてみよう。
AppDelegate.swiftの「部活用の管理オブジェクトコンテキスト」を以下のコードに変更する。
parentContextプロパティに生徒用のコンテキストを設定、コーディネーターを削除、キューをプライベートキューに変更した。
1 2 3 4 5 6 7 |
//部活用の管理オブジェクトコンテキスト lazy var contextClub: NSManagedObjectContext = { var contextClub = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType) contextClub.parentContext = self.contextStudent return contextClub }() |
以下は実際のプレイ動画。顧問を「鬼瓦」に変更し、保存ボタンを押した段階で生徒オブジェクトのリレーションシップも「鬼瓦」になった。
マージされた子コンテキストのオブジェクトが外部ファイルに保存されるのは、親コンテキストの保存メソッドが呼び出されたときになるのも覚えておこう。