【Swift】Core Dataの使い方。エンティティごとに出力先の外部ファイルを変える。(Swift 2.1、XCode 7.2)
CONFIGURATIONSとは
本記事では、データモデルの「CONFIGURATIONS(以下、コンフィグレーション)」について説明する。
コンフィグレーションとは、外部ファイルに出力するエンティティをまとめた定義のことである。デフォルトでは1つのコンフィグレーションの中に、すべてのエンティティが記載されている。
なので、前回記事までは、Core Dataを使ってデータを保存するときの出力先の外部ファイル(.sqlite)は1個である。
コンフィグレーションを増やしてエンティティを振り分けると、エンティティごとに出力先の外部ファイルを変えられるようになる。
コンフィグレーションの追加を試す
実際にコンフィグレーションを追加して、出力先の外部ファイル(.sqlite)をエンティティごとに分けてみよう。
以降の手順を行う前のXcodeプロジェクトをGitHubに置いたので、試してみる人はご利用されたし。
⇒「テスト用プロジェクト」
生徒と教師のデータをCoreDataを使って外部ファイルに保存するものを実装しておいた。この段階では出力先の外部ファイルはまだ1つである。
以下は検証用プロジェクトの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 |
// // ViewController.swift // import UIKit import CoreData class ViewController: UIViewController, UITextFieldDelegate { @IBOutlet weak var studentTextField: UITextField! @IBOutlet weak var sexSegument: UISegmentedControl! @IBOutlet weak var teacherTextField: UITextField! @IBOutlet weak var subjectTextField: UITextField! //管理オブジェクトコンテキスト var context:NSManagedObjectContext! //最初からあるメソッド override func viewDidLoad() { super.viewDidLoad() //部活動用と生徒用の管理オブジェクトコンテキストを取得する。 let applicationDelegate = UIApplication.sharedApplication().delegate as! AppDelegate context = applicationDelegate.managedObjectContext //デリゲート先を自分に設定する。 studentTextField.delegate = self teacherTextField.delegate = self subjectTextField.delegate = self //外部ファイルの出力先ディレクトリを表示する。 print(NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.AllDomainsMask, true).first!) } //Returnキー押下時の呼び出しメソッド func textFieldShouldReturn(textField: UITextField) -> Bool { //キーボードをしまう。 studentTextField.endEditing(true) teacherTextField.endEditing(true) subjectTextField.endEditing(true) return true } //生徒登録ボタン押下時の呼び出しメソッド @IBAction func pushStudentBtn(sender: UIButton) { do { //生徒オブジェクトをフェッチする。 let fetchRequest = NSFetchRequest(entityName: "Student") fetchRequest.predicate = NSPredicate(format:"name = %@", studentTextField.text!) let studentList = try context.executeFetchRequest(fetchRequest) as! [Student] let student:Student if(studentList.count == 0) { //フェッチできなかった場合は生徒用の管理オブジェクトコンテキストに新規追加する。 student = NSEntityDescription.insertNewObjectForEntityForName("Student", inManagedObjectContext: context) as! Student } else { //フェッチできた場合はそのオブジェクトを編集する。 student = studentList[0] } //生徒オブジェクトの属性値を変更する。 student.name = studentTextField.text! student.sex = sexSegument.selectedSegmentIndex //データを保存する。 try context.save() //テキストフィールドを空にする。 studentTextField.text = "" } catch { print(error) } } //教師登録ボタン押下時の呼び出しメソッド @IBAction func pushTeacherBtn(sender: UIButton) { do { //教師オブジェクトをフェッチする。 let fetchRequest = NSFetchRequest(entityName: "Teacher") fetchRequest.predicate = NSPredicate(format:"name = %@", teacherTextField.text!) let teacherList = try context.executeFetchRequest(fetchRequest) as! [Teacher] let teacher:Teacher if(teacherList.count == 0) { //フェッチできなかった場合は生徒用の管理オブジェクトコンテキストに新規追加する。 teacher = NSEntityDescription.insertNewObjectForEntityForName("Teacher", inManagedObjectContext: context) as! Teacher } else { //フェッチできた場合はそのオブジェクトを編集する。 teacher = teacherList[0] } //生徒オブジェクトの属性値を変更する。 teacher.name = teacherTextField.text! teacher.subject = subjectTextField.text! //データを保存する。 try context.save() //テキストフィールドを空にする。 teacherTextField.text = "" subjectTextField.text = "" } catch { print(error) } } } |
デフォルトのコンフィグレーションを使った場合
まず、プレイ動画で保存した生徒「田中、女性」と教師「佐野、数学」の外部ファイルを確認し、デフォルトのコンフィグレーション(外部ファイルが1個)の場合、どのように保存されるのかを確かめる。
Xcodeのデバッグエリアに表示されている外部ファイルのフォルダパスをコピーする。
Macの「LaunchPad」⇒「その他」からターミナルを起動する。
ターミナル画面で「cd フォルダパス」を叩いて、外部ファイルの場所まで移動する。続いて、lsコマンドを叩くと、出力された外部ファイル(.sqlite)が1つ見つかる。
1 2 3 4 5 6 7 |
$ cd /Users/test/Library/Developer/CoreSimulator/Devices/D3..(省略) $ $ ls SingleViewCoreData.sqlite SingleViewCoreData.sqlite-wal SingleViewCoreData.sqlite-shm |
「sqlite3 外部ファイル名」を叩いてsqlite3を起動する。sqlite3とは、SQLiteデータベースの内容を確認編集するためのコナンドラインユーティリティーのことで、最近のMacには標準インストールされている。
「.tables」を叩いて保存されているテーブルの一覧を表示する。ZSTUDENTが生徒テーブル、ZTEACHERが教師テーブルである。
1 2 3 4 5 6 7 8 |
$ sqlite3 SingleViewCoreData.sqlite SQLite version 3.8.10.2 2015-05-20 18:17:19 Enter ".help" for usage hints. sqlite> .tables ZSTUDENT ZTEACHER Z_METADATA Z_MODELCACHE Z_PRIMARYKEY |
「select * from ZSTUDENT;」を叩いてテーブルの中身を表示する。登録した生徒「田中」が表示された。
データが見づらいので表示を変更しよう。
「.header on」を叩いて、テーブルの列名が表示されるようにする。
「.mode column」を叩いて、列の幅が見やすいように調整されるようにする。
そして、再度「select * from ZSTUDENT;」を叩くと以下のように見やすい表示になる。
1 2 3 4 5 6 7 8 9 10 11 |
sqlite> select * from ZSTUDENT; 1|1|1|1|田中 sqlite> .header on sqlite> .mode column sqlite> select * from ZSTUDENT; Z_PK Z_ENT Z_OPT ZSEX ZNAME ---------- ---------- ---------- ---------- ---------- 1 1 1 1 田中 |
見慣れない列があるので各列の意味を参考までに説明しておく。
「Z_PK」はプライマリキーのことで、データを追加するごとに1から順番に番号が振られる。
「Z_ENT」はテーブルごとに振られる連番で、同一テーブル内のZ_ENTはすべて同じ数字になる。
「Z_OPT」はレコードの更新回数のことで、レコードが更新される度に数字が1増加していく。
ZSEXは性別、ZNAMEは名前の列である。
教師のテーブルを確認する。「select * from XTEACHER;」を叩くと教師「佐野、数学」が登録されていることが分かる。
1 2 3 4 5 6 |
sqlite> select * from ZTEACHER; Z_PK Z_ENT Z_OPT ZNAME ZSUBJECT ---------- ---------- ---------- ---------- ---------- 1 2 1 佐野 数学 |
2つのコンフィグレーションを使った場合
上記の作業により、1つの外部ファイルの中に、生徒と教師のデータが保存されていることが確認された。
次に、生徒用と教師用の2つのコンフィグレーションを作り、出力先の外部ファイルを2つに分ける。
下図赤枠の「プロジェクト名.xcdatamodeld」を選択、黄緑枠の「+」ボタンを長押しして吹き出しのメニューを表示し、「Add Configuration」を選択する。これを2回行う。
すると、下図赤枠枠にコンフィグレーションが2つ追加されるので、1つずつダブルクリックして編集状態にし、名前を「StudentConfig」と「TeacherConfig」にする。
StudentConfigを選択し、黄緑枠のStudentエンティティをコンフィグレーションの定義にドラッグ&ドロップで入れる。Teacherエンティティも同じようにしてTeacherConfigに入れる。これで、生徒用と教師用の2つのコンフィグレーションができた。
ちなみに、Defaultのコンフィグレーションはもう使わないので消したいところだが、消すことはできない。
AppDelegate.swiftのpersistentStoreCoordinateorプロパティを以下に変更する。
生徒用と教師用の出力先URLを定義したのと、コーディネーターに生徒用と教師用の永続ストアを追加した(青色箇所)。
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 |
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { // コーディネータを作成する。 let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) //生徒用と教師用のurlを作成する。 let urlStudent = self.applicationDocumentsDirectory.URLByAppendingPathComponent("StudentCoreData.sqlite") let urlTeacher = self.applicationDocumentsDirectory.URLByAppendingPathComponent("TeacherCoreData.sqlite") var failureReason = "There was an error creating or loading the application's saved data." do { //コーディネーターに生徒用と教師用の永続ストアを追加する。 try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: "StudentConfig", URL: urlStudent, options: nil) try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: "TeacherConfig", URL: urlTeacher, options: nil) } catch { // エラー処理 var dict = [String: AnyObject]() dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" dict[NSLocalizedFailureReasonErrorKey] = failureReason dict[NSUnderlyingErrorKey] = error as NSError let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict) NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)") abort() } return coordinator }() |
出力先外部ファイルが変わって過去の外部ファイルを残しておくのは紛らわしいので、アプリを一旦削除してからシュミレーターを実行しよう。
ちなみに、アプリを削除する方法は、以下の動画のようにアイコンを長押しして×ボタン。
以下は実際のプレイ動画
先ほどと同じ手順で外部ファイルを見に行くと。。。外部ファイル(.sqlite)が2つに増えた。
1 2 3 4 5 6 7 8 |
$ cd /Users/test/Library/Developer/CoreSimulator/Devices/D3116..(省略) $ $ ls StudentCoreData.sqlite TeacherCoreData.sqlite StudentCoreData.sqlite-shm TeacherCoreData.sqlite-shm StudentCoreData.sqlite-wal TeacherCoreData.sqlite-wal |
生徒用の外部ファイル(StudentCoreData.sqlite)の中身を確認すると、生徒用と教師用の両方のテーブルがあるが、データは生徒テーブルのみが持っていることが分かる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ sqlite3 StudentCoreData.sqlite SQLite version 3.8.10.2 2015-05-20 18:17:19 Enter ".help" for usage hints. sqlite> .header on sqlite> .mode column sqlite> .tables ZSTUDENT ZTEACHER Z_METADATA Z_MODELCACHE Z_PRIMARYKEY sqlite> select * from ZSTUDENT; Z_PK Z_ENT Z_OPT ZSEX ZNAME ---------- ---------- ---------- ---------- ---------- 1 1 1 1 田中 sqlite> select * from ZTEACHER; sqlite> |
教師用の外部ファイル(TeacherCoreData.sqlite)の中身を確認すると、こちらにも生徒用と教師用の両方のテーブルがあるが、データは教師テーブルのみが持っていることが分かる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ sqlite3 TeacherCoreData.sqlite SQLite version 3.8.10.2 2015-05-20 18:17:19 Enter ".help" for usage hints. sqlite> .header on sqlite> .mode column sqlite> .tables ZSTUDENT ZTEACHER Z_METADATA Z_MODELCACHE Z_PRIMARYKEY sqlite> select * from ZSTUDENT; sqlite> select * from ZTEACHER; Z_PK Z_ENT Z_OPT ZNAME ZSUBJECT ---------- ---------- ---------- ---------- ---------- 1 2 1 佐野 数学 2 2 1 鬼瓦 体育 sqlite> |
以上、コンフィグレーションを増やしてエンティティごとの出力先を分ける方法を説明した。
1つ注意点があり、異なるコンフィグレーションのオブジェクト同士はリレーションシップを追加できないということだ。なので、将来的なエンティティ間のリレーション追加も考慮してコンフィグレーション分けの有無を判断するべし。
コンフィグレーション分けがどれだけ性能アップに寄与するのかも気になるところなので今後の課題とする。