【Swift 3】CoreDataをつかってみた。Relationshipをつけよう【part3】
どうも。part3です。
part1,2はそれぞれ以下よりどうぞ。
【Swift 3】CoreDataを使ってみた【part1】
【Swift 3】CoreDataを使ってみた。検索・更新・削除編【part2】
今回はRelationshipをつけていきます。
Relationshipとは
Relationshipとは、Entity間で関係を持たせることです。
CoreDataは「関係データベース(Relational Database)」の類ですし、それぞれのEntityに関係を持たせることができます。
「関係データベース」ってなんだ?って人はまずはそこから勉強すると良いと思います。
エラそうに言っていますが、私もFE(資格)の勉強をする時にやっただけで、実際に触ってみるのは初めてです!
少しだけ説明しておこうと思います。
関係データベースとは
関係データベースは、データを行と列による2次元の表で表し、複数の表を組み合わせてデータを管理します。
こんな感じのやつです
関係データベースには「主キー」と「外部キー」があります。
主キーと外部キー
主キーは「行を一意に識別するための列」のことをいいます。
つまり「データの値が空でなく、なおかつ決して重複しない列 」のことです。
この主キーがあることによって確実にデータを識別することができます。
外部キーは「他の表の主キーを参照する列」です。
別の表の主キーとなっているデータが格納されています。
CoreDataを使用するときも同様に、1つのEntityに必ず1つ主キー にあたるものを作成してください。
外部キーはRelationshipを指定するときに必要です。
正規化とは
正しくデータが管理できるような表を作ることを正規化といいます。
関係データベースでは、表をいくつも組み合わせてデータを管理しています。
それぞれの表の主キーや外部キーが変更された際には、他の表にも反映しなくてはなりません。
そうしなければ、データに矛盾や重複が発生しています。
正規化することで、データの矛盾や重複を取り除くことができます。
正規化をすすめると、上のようにいくつもの表に分けることになるので、毎回表の結合をする必要が出てくるので、アクセス効率はむしろ低下してしまいます。
厳密にいうと上記表の上側の表も正規化された表になります(第2正規形)。おそらく下の表も、担任→教室の部分が別表に分けられると思いますので、第2正規形なのかな・・・。
ちなみに正規化されていない表というのは下のようなものになります。
下は第1正規形となります。
正規化は
1.繰り返し項目がある(非正規形)
2.繰り返し項目を取り除く(第1正規形)
3.主キーによって他の項目が決まるように分別(第2正規形)
4.主キー以外の項目によっても決定される項目を分別(第3正規形)
といった流れになります。
CoreDataで複数の表をつくろう
長くなりましたが、やっとCoreDataの方をやっていきます。
CoreDataで上記の「学生表」と「担任表」を作っていこうと思います。
まずはEntityを作成していきます。
Add Entityより2つのEntityを作成します。
このEntityの名前は表のタイトル(学生表/担任表)だと思うとわかりやすいかなと思います。
StudentとClassにそれぞれAttributeを付け加えていきます。
Student
Class
Attributesは上記表の「学籍番号 / 氏名 / クラス」の部分になります。
Relationshipをつけよう
StudentとClassにRelationshipをつけていきます。
まずは Student側
Class側
Student側に付け足した「studentClass」は先ほど説明した外部キーになります。
私は「AttributeがstudentClassでTypeがClass」といった覚え方をしています。分かりづらいですが、このstudentClassもその他のnameとかと同じように扱うといった認識をしています。。
Class側は「Student表にあるstudentClassから参照されているよ」といった関係がつけられています。
間違った認識だったら教えて下さい(´;ω;`)
1対1、1対多の関係
CoreDataのRelationshipといったらこの「1対1の関係」「1対多の関係」「多対多の関係」といった言葉をよく聞きますよね。
これはつまりこういうことだと思っています。
「相田あい」の担任は「坂井聡」です。そのほかに担任はいません。なのでこれは1対1の関係。
反対に
「坂井聡」の担任するクラスの学生は「相田あい」以外にもたくさんいます。なのでこれは1対多の関係。
これをCoreDataで関係をつけていきます。
Student
右側のRelationshipよりType > To Oneを選択
Class
右側のRelationshipよりType > To Manyを選択
これでDataModelの設定は完了です。
Editor > Create NSManagedObject Subclassよりサブクラスを作成しておいてください。
データを保存しよう
データを保存していきます。全体のコードから
import CoreData
/* ~~  */ 
   //データを保存する
    func saveData(){
        let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
        let context:NSManagedObjectContext = appDelegate.managedObjectContext
        let entityClass = NSEntityDescription.entity(forEntityName:"Class",in:context)
        let className = NSManagedObject(entity:entityClass!,insertInto:context) as! Class
        
        className.studentClass = "1A"
        className.classRoom = "101"
        className.teacher = "坂井聡"
        
        let entityStudent = NSEntityDescription.entity(forEntityName:"Student",in:context)
        let student = NSManagedObject(entity:entityStudent!,insertInto:context) as! Student
        
        student.name = "相田あい"
        student.studentNumber = 1
        
/*  ここから修正
        //クラス名を検索して、studentプロパティに設定する
        let fetchRequest:NSFetchRequest<Class> = Class.fetchRequest()
        fetchRequest.predicate = NSPredicate(format:"%K == %@","studentClass","1A")
        let fetchData = try! context.fetch(fetchRequest)
        if(!fetchData.isEmpty){
            student.studentClass = fetchData[0]
        }
*/
        //多分こっちでないと紐付けできない
        student.studentClass = className
        
        do{
            try context.save()
        }catch{
            print(error)
        }
        
    }
かるく説明をしていきます。
まず最初に、Classの方にデータをいれておきます。
let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
let context:NSManagedObjectContext = appDelegate.managedObjectContext
let entityClass = NSEntityDescription.entity(forEntityName:"Class",in:context)
let className = NSManagedObject(entity:entityClass!,insertInto:context) as! Class
        
className.studentClass = "1A"
className.classRoom = "101"
className.teacher = "坂井聡"
次にStudentを登録していきます。
let entityStudent = NSEntityDescription.entity(forEntityName:"Student",in:context)
let student = NSManagedObject(entity:entityStudent!,insertInto:context) as! Student
        
student.name = "相田あい"
student.studentNumber = 1
そして、StudentとClassを紐付けます
/* 多分これじゃダメ
let fetchRequest:NSFetchRequest<Class> = Class.fetchRequest()
fetchRequest.predicate = NSPredicate(format:"%K == %@","studentClass","1A")
let fetchData = try! context.fetch(fetchRequest)
if(!fetchData.isEmpty){
     student.studentClass = fetchData[0]
}
*/
すでにあるクラスに登録する場合は最初のクラスの登録はしなくていいです。
紐づけるときに、クラスが存在しない場合はstudentClassには何も挿入されません(nilになる)
紐付けはおそらく
student.studentClass = classNameだけでおkです。
上記だと、まだデータの保存ができていないのでfetchDataは必ず空になっているのではと思います。。。
(2017/03/15日追記)
データを呼び出そう
データを呼び出すときはRelationshipをつけていないときとほとんど同じです。
例えば、「相田あいさんの担任が知りたい」とき
func readData(){
      //読み込む
      let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
      let context:NSManagedObjectContext = appDelegate.managedObjectContext
      let fetchRequest:NSFetchRequest<Student> = Student.fetchRequest()
      let predicate = NSPredicate(format:"%K == %@","name","相田あい")
      fetchRequest.predicate = predicate
      let fetchData = try! context.fetch(fetchRequest)
      if(!fetchData.isEmpty){
          for i in 0..<fetchData.count{
              print(fetchData[i].name)
              //担任の名前を表示
              print(fetchData[i].studentClass?.teacher)
          }
          //書き換えをした場合は保存 してないなら必要ないよ
          do{
              try context.save()
          }catch{
              print(error)
          }
      }
}
通常のAttributeとそんなに変わらず使えます。
ただこれはあんまり良くない調べ方だと思います(相田あいという名前が重複している可能性があるため)
反対に、「坂井聡さんが担任をするクラスの生徒」を表示するとき
    func readData(){
        let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
        let context:NSManagedObjectContext = appDelegate.managedObjectContext
        let fetchRequest:NSFetchRequest<Class> = Class.fetchRequest()
        let predicate = NSPredicate(format:"%K == %@","teacher","坂井聡")
        fetchRequest.predicate = predicate
        let fetchData = try! context.fetch(fetchRequest)
        if(!fetchData.isEmpty){
            for i in 0..<fetchData.count{
                //クラスの人数を取得
                print(fetchData[i].student?.count)
            }
            //書き換えをした場合は保存
            do{
                try context.save()
            }catch{
                print(error)
            }
        }
    }この場合
fetchData[i].student?.nameといった書き方はできません。
クラスの人数を調べる場合は簡単にできますが、
クラス側からの場合は、student個人の変更はできません。(できるかもですが面倒そう・・・)
----(2017/09/08追記)-----
クラス側からstudentの情報を取り出すときは取得したfetchData[i]の情報を使えば取得できます。
上記のfor文の中は[Class]の中身を1つずつ取り出しているのでfetchData[i]はClassの情報が入っています。
なのでfor文の中で
            for i in 0..<fetchData.count{
                let fetchStudent:NSFetchRequest<Student> = Student.fetchRequest()
                fetchStudent.predicate = NSPredicate(format:"%K == %@","studentClass",fetchData[i])
                let studentData = try! context.fetch(fetchStudent)
                if(!studentData.isEmpty){
                    for student in studentData {
                        print(student.name)
                    }
                }
            }
と、Studentを探して来てやればおkです。
このときClassに紐づいているStudentを全て取得する場合(この場合だと坂井聡さんが担当しているクラスの生徒)、predicateのフォーマットの部分でfetchData[i]を用います。
この"studentClass"というキーは、モデルを作ったときに設定した外部キーってやつです。
ファイルを見た方がわかりやすいかもしれない。
この studentClass:Class?ってなってるやつですね。
(改めて読んでみると、このときはfor in文で配列の中身を1つずつ取り出す方法も知らなかったっぽいですね。あとはひっそり読み込みの方の保存方法がおかしかったのを修正しました。更新してなければ呼ぶ必要はないところなので支障はなかったと信じる・・・)
--------------------------------
データを削除しよう
studentの中から1人を削除する場合は通常と同じ方法でOKです
func deleteData(){
    let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
    let context:NSManagedObjectContext = appDelegate.managedObjectContext
    let fetchRequest:NSFetchRequest<Student> = Student.fetchRequest()
    let predicate = NSPredicate(format:"%K = %@","name","相田あい")
    fetchRequest.predicate = predicate
    let fetchData = try! context.fetch(fetchRequest)
    if(!fetchData.isEmpty){
        for i in 0..<fetchData.count{
            let deleteObject = fetchData[i] as Student
            context.delete(deleteObject)
        }
        do{
            try context.save()
        }catch{
            print(error)
        }
    }  
}
このときClass側からの参照も削除されているはずです。(設定によってはされないらしい)
Classを削除する場合は
func deleteClass(){
    let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
    let context:NSManagedObjectContext = appDelegate.managedObjectContext
    let fetchRequest:NSFetchRequest<Class> = Class.fetchRequest()
    let predicate = NSPredicate(format:"%K = %@","studentClass","1A")
    fetchRequest.predicate = predicate
    let fetchData = try! context.fetch(fetchRequest)
    if(!fetchData.isEmpty){
        for i in 0..<fetchData.count{
            print(fetchData[i])
            let deleteObject = fetchData[i] as Class
            context.delete(deleteObject)
        }
        do{
            try context.save()
        }catch{
            print(error)
        }
    }
 }
この場合1Aというクラスは削除されますが、1Aに所属しているStudentは削除されません。
studentClass = "1A"の生徒は studentClass = nil となってしまいます。
CoreDataについてはとりあえずこのパートで終わる予定です。
また何かあったら個別に書いていこうと思います。
長くなってしまいましたがなんだかそんなにうまく説明できた気もしないです。。。
参考
CoreDataについては、「はじはじアプリ製作体験記」さんの記事が個人的にはすごくわかりやすくて大変参考になりました。ありがとうございます。
細かい設定などに関しても詳しく書かれていますのでぜひぜひ。
参考リンク
追記
(追記日: 2018-04-13)
Swift4で動作チェック済みです。
少し気になったところですが、本記事のsaveData()を繰り返して呼び出すと、1Aのクラスが何個も作られることになります。
例えば、既にある1Aのクラスに生徒を追加する時は
    func addStudentData() {
        let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
        let context:NSManagedObjectContext = appDelegate.managedObjectContext
        let fetchRequest:NSFetchRequest = Class.fetchRequest()
        let predicate = NSPredicate(format:"%K == %@","studentClass","1A")
        fetchRequest.predicate = predicate
        let fetchData = try! context.fetch(fetchRequest)
        if(!fetchData.isEmpty){
            for classData in fetchData{
                //ここで生徒を追加
                let fetchStudent:NSFetchRequest = Student.fetchRequest()
                fetchStudent.predicate = NSPredicate(format:"%K == %@","studentClass",classData)
                let entityStudent = NSEntityDescription.entity(forEntityName:"Student",in:context)
                let student = NSManagedObject(entity:entityStudent!,insertInto:context) as! Student
                student.name = "餅もちこ"
                student.studentNumber = 2
                student.studentClass = classData
            }
            //書き換えをした場合は保存
            do{
                try context.save()
            }catch{
                print(error)
            }
        }else {
            //なければ新しく作るとか
        }
    }こんな感じで一度クラスのデータを読み込む必要があります。
あとは className.studentClass = "1A" student.studentClass = className
となっているところ、別にお互いのstudentClassという名前にまったく関係性ないですね。すんごいわかりづらい名前つけちゃってますが。(っていうか多分当時は関係あると思ってたのかな・・・)
なのでstudent.studentClass = "1A"としたら繋がりがあるわけではありません。 あくまで、Class型のオブジェクトを参照することになります。
というかもうClassっていう名前をつけたのも問題というか・・・せめてClassRoomって型名にするべきでした。
どうしてもわかりづらくて耐えられなくなったら全部置き換えするかもしれませんが、今はこの追記だけで失礼します。

 
   
   
   
   
   
   
   
   
   
   
   
   
   
  