リレーションシップを使ってエンティティ同士をリンクする方法を解説します。
環境
この記事の情報は次のバージョンで動作確認しています。
【Swift】5.3.2
【iOS】14.4
【macOS】Big Sur バージョン 11.1
リレーションシップの設定
リレーションシップとは、レコード(オブジェクト)に別のエンティティのオブジェクトを関連付ける仕組みです。
1対1のリレーション
例として次のような2つのテーブルの関係を考えてみます。
生徒が所属するクラブを、Clubテーブルへのリレーションシップで表した例です。
一人の生徒が所属できるのは1つのクラブだけです。
これをエンティティの定義に置き換えて見ましょう。
Clubエンティティは次の通りです。
Studentエンティティの所属クラブを示す項目(club)は、次のようにRelationshipとして定義します。Destination(接続先)はClubエンティティを指定します。
これで、StudentからClubへのリレーションシップが定義できました。
1対多のリレーション
Student側からだけでなく、Club側からStudentへの関係も辿れた方が便利ですね。
ClubからStudentへのリレーションシップも定義してみましょう。
ClubエンティティのRelationshipに、所属する生徒を表すstudentsを定義し、Destination(接続先)にStudentエンティティを指定します。
1つのクラブに対して所属できる生徒は複数いますので、1対多の関係する必要があります。
studentsのInspectorを開き、TypeをデフォルトのTo OneからTo Manyに変更します。
これでリレーションシップstudentsはStudentオブジェクトを複数所持可能なリストとして定義されます。
これでClubからStudentへのリレーションシップが設定完了です。
2つのテーブルの関係は次にようになります。
相互リレーション
上記で設定した2つのリレーションの相互リレーション(Inverse)を定義すると、片方のオブジェクトを追加/削除した場合、関係するリレーションが自動で更新されるようになります。
こちらは原則定義しておきましょう。(この定義が無いとBuild時にWarningが出ます)
リレーションシップstudentsのInverseをclubに設定します。
反対側のStudentsエンティティを見ると、リレーションシップclubのInverseが自動的に設定されているのがわかります。
これで、相互リレーションの定義が完了です。
リレーションデータの登録
次に、リレーションデータの登録方法を解説します。
次の初期データ登録処理(RegistSampleData.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 |
import CoreData func registSampleData(context: NSManagedObjectContext) { /// Studentテーブル初期値 let studentList = [ ["001", "カピバラ", "2010/04/16", "3", "A", "バスケット"], ["002", "アライグマ", "2011/02/06", "0", "B", "サッカー"], ["003", "カイウサギ", "2010/04/08", "10", "B", ""], ["004", "ハクビシン", "2010/12/21", "7", "A", "吹奏楽"], ["005", "ワオキツネザル", "2010/9/20", "5", "A", "サッカー"] ] /// Clubテーブル初期値 let clubList = [ ["バスケット", "ツキノワグマ"], ["サッカー", "ベンガルトラ"], ["陸上", "ニホンジカ"], ["吹奏楽", "アカカンガルー"] ] /// Studentテーブル全消去 let fetchRequestStudent = NSFetchRequest<NSFetchRequestResult>() fetchRequestStudent.entity = Student.entity() let students = try? context.fetch(fetchRequestStudent) as? [Student] for student in students! { context.delete(student) } /// Clubテーブル全消去 let fetchRequestClub = NSFetchRequest<NSFetchRequestResult>() fetchRequestClub.entity = Club.entity() let clubs = try? context.fetch(fetchRequestClub) as? [Club] for club in clubs! { context.delete(club) } /// Clubテーブル登録 for club in clubList { let newClub = Club(context: context) newClub.clubName = club[0] // クラブ名 newClub.teacher = club[1] // 担任 } let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy/M/d" /// Studentテーブル登録 for student in studentList { let newStudent = Student(context: context) newStudent.sid = student[0] // 生徒番号 newStudent.name = student[1] // 氏名 newStudent.birthday = dateFormatter.date(from: student[2])! // 生年月日 newStudent.absentDays = Int16(student[3])! // 欠席日数 newStudent.nameOfClass = student[4] // クラス名 /// リレーションの設定 fetchRequestClub.predicate = NSPredicate(format: "clubName = %@", student[5]) let result = try? context.fetch(fetchRequestClub) as? [Club] if result!.count > 0 { /// Student -> Clubへのリレーション newStudent.club = result![0] } } /// コミット try? context.save() } |
1対1のリレーション追加
リレーションの追加処理はコードの次の部分です。
1 2 3 4 5 6 7 8 9 |
/// リレーションの設定 fetchRequestClub.predicate = NSPredicate(format: "clubName = %@", student[5]) let result = try? context.fetch(fetchRequestClub) as? [Club] if result!.count > 0 { /// Student -> Clubへのリレーション newStudent.club = result![0] } |
Clubテーブルより対象となるクラブのレコードを検索し、新規に作成したStudentオブジェクトに関連付けています。
StudentからみたClubは1対1の関係です。(生徒一人が所属するクラブは1つのみ)
1対nのリレーション追加
StudentとClubは相互リレーションの関係ですので、逆からのリレーション追加も可能です。
先程のコードを次にのように変更しても、同じ結果が得られます。
※強調部分が変更点です
1 2 3 4 5 6 7 8 9 10 |
/// リレーションの設定 fetchRequestClub.predicate = NSPredicate(format: "clubName = %@", student[5]) let result = try? context.fetch(fetchRequestClub) as? [Club] if result!.count > 0 { /// Club -> Studentへのリレーション result![0].addToStudents(newStudent) } } |
ClubからみたStudentは1対nの関係(1つのクラブに所属する生徒は複数)ですので、リレーションはコレクション形式で保持されます。
追加/削除にはアクセサメソッドを使用します。
Clubエンティティのソース(Club+CoreDataProperties.swift)を手動生成して確認すると、Club.studentsに登録・削除する為の、次のようなアクセサメソッドが追加されているのがわかります。
(※コードの手動生成方法は 【SwiftUI】Core Dataの使い方:エンティエィ(Entity)を定義する を参照して下さい。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// MARK: Generated accessors for students extension Club { @objc(addStudentsObject:) @NSManaged public func addToStudents(_ value: Student) @objc(removeStudentsObject:) @NSManaged public func removeFromStudents(_ value: Student) @objc(addStudents:) @NSManaged public func addToStudents(_ values: NSSet) @objc(removeStudents:) @NSManaged public func removeFromStudents(_ values: NSSet) } |
リレーションデータの取得
次にリレーションデータの取得方法を解説します。
1対1のリレーション取得
Studentテーブルから関連するClubテーブルの情報を検索する例です。
1対1のリレーション取得になります。
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 |
struct ContentView: View { @Environment(\.managedObjectContext) private var context /// データ取得処理 @FetchRequest( entity: Student.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Student.sid, ascending: true)] ) private var students: FetchedResults<Student> var body: some View { List { ForEach(students, id: \.self) { student in Section(header: HStack { Text("\(student.sid!) \(student.name!)") }){ VStack(alignment: .leading) { Text("生年月日:\(student.birthday!, style: Text.DateStyle.date)") Text("欠席日数:" + String(student.absentDays)) Text("クラス:\(student.nameOfClass!)") HStack { Text("部活:") if let club = student.club { Text("\(club.clubName!) (顧問:\(club.teacher!))") .foregroundColor(.red) } } } } } } .onAppear { /// Listビュー表示時に初期データ登録処理を実行 registSampleData(context: context) } } } |
次の箇所が、リレーションの取得部分になります。
リレーションを示すstudent.clubがClubオブジェクトそのものです。
1 2 3 4 5 6 7 8 9 |
HStack { Text("部活:") if let club = student.club { Text("\(club.clubName!) (顧問:\(club.teacher!))") .foregroundColor(.red) } } |
1対nのリレーション取得
こちらはClubテーブルから関連するStudentテーブルの情報を検索する例です。
1対nのリレーション取得になります。
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 |
struct ContentView: View { @Environment(\.managedObjectContext) private var context /// データ取得処理 @FetchRequest( entity: Club.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Club.clubName, ascending: true)] ) private var clubs: FetchedResults<Club> var body: some View { List { ForEach(clubs) { club in Section(header: HStack { Text("\(club.clubName!) (顧問:\(club.teacher!))") }){ VStack(alignment: .leading) { ForEach(studentArray(club.students), id: \.self) { student in Text("\(student.sid!) \(student.name!)") } } } } } .onAppear { /// Listビュー表示時に初期データ登録処理を実行 registSampleData(context: context) } } /// NSSet? → [Student]変換 private func studentArray(_ students: NSSet?) -> [Student] { let set = students as? Set<Student> ?? [] return set.sorted { $0.sid! < $1.sid! } } } |
Studentテーブルへのリレーションを示す club.students は、順序付けされていないコレクションクラスであるNSSet型なので、次の関数でStudentの配列に変換しています。
1 2 3 4 5 6 7 8 9 |
/// NSSet? → [Student]変換 private func studentArray(_ students: NSSet?) -> [Student] { let set = students as? Set<Student> ?? [] return set.sorted { $0.sid! < $1.sid! } } |
今回はサンプルとしてわかりやすいように、ContentViewのプライベート関数としましたが、次のようにClubエンティティクラス(Club+CoreDataProperties.swift)の計算プロパティとして追加した方が使い勝手が良いでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
extension Club { @nonobjc public class func fetchRequest() -> NSFetchRequest<Club> { return NSFetchRequest<Club>(entityName: "Club") } @NSManaged public var clubName: String? @NSManaged public var teacher: String? @NSManaged public var students: NSSet? public var studentArray: [Student] { let set = students as? Set<Student> ?? [] return set.sorted { $0.sid! < $1.sid! } } } |