SwiftData: Illegal attempt to establish a relationship 'student' between objects in different contexts

265 views Asked by At

In the following program, I assumed that swift data would automagically handle all relationships. When a new exam is created, a roster is passed in and the intention is for all grades to be initialized to 0 for all students in the roster.

import SwiftUI
import SwiftData

extension String {
    static func randomString(length: Int = 7) -> String {
        let letters = "abcdefghijklmnopqrstuvwxyz"
        return String((0..<length).map{ _ in letters.randomElement()! })
    }
}


@Model
final class Grade {
    var student : Student
    var grade : Int
    
    init(student: Student, grade: Int) {
        self.student = student
        self.grade = grade
    }
}

@Model
final class Exam {
    var name : String
    var date : Date
    var grades : [Grade]
    
    init(name: String, date: Date, roster: [Student]) {
        self.name = name
        self.date = date
        
        // initialize grades to 0 for every student in the given roster...
        
        // this next line causes the following....
        //
        //        *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
        //        reason: 'Illegal attempt to establish a relationship 'student' between objects in different contexts
        //        (source = <NSManagedObject: 0x600002217f70>
        //            (entity: Grade; id: 0x6000002b90a0 <x-coredata:///Grade/tB22D0958-A107-4FA1-B705-CA54127FB53232>; data: {
        //            grade = nil;
        //            student = nil;
        //        }) ,
        //         destination = <NSManagedObject: 0x6000021dd360>
        //            (entity: Student; id: 0xa40ac2d6f4136136 <x-coredata://5F5F57E9-7355-46B0-9143-D08161C9FA59/Student/p3>; data: {
        //            first = cmzngpm;
        //            last = odzlkmv;
        //        }))'

        self.grades = roster.map( {Grade(student: $0, grade: 0)} )
    }
}

@Model
final class Student {
    var first : String
    var last : String
    
    init(first: String, last: String) {
        self.first = first
        self.last = last
    }
}


@Model
final class Course {
    var name : String
    var roster : [Student]
    var exams : [Exam]
    
    init(name: String, roster: [Student]) {
        self.name = name
        self.roster = roster
        self.exams = []
    }
}

@main
struct StudentsApp: App {
    var sharedModelContainer: ModelContainer = {
        let schema = Schema([Course.self, Student.self, Exam.self, Grade.self])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()

    var body: some Scene {
        WindowGroup {
            CourseListView()
        }
        .modelContainer(sharedModelContainer)
    }
}



struct CourseListView : View {
    @Environment(\.modelContext) private var modelContext
    @Query private var courses: [Course]
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(courses) { course in
                    NavigationLink {
                        CourseView(course: course)
                    } label: {
                        Text(course.name)
                    }
                }
            }
            .navigationTitle("Courses")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button(action: {addCourse()}, label: {
                        Image(systemName: "plus")
                    })
                }
            }
        }
    }
    
    private func addCourse() {
        modelContext.insert(Course(name: String.randomString(), roster: []))
    }
}

struct CourseView: View {
    @Environment(\.modelContext) private var modelContext
    @Bindable var course : Course

    var body: some View {
        NavigationStack {
            List {
                Section {
                    ForEach(course.exams) { exam in
                        NavigationLink {
                            ExamView(course: course, exam: exam)
                        } label: {
                            Text(exam.name)
                        }
                    }
                } header: {
                    HStack {
                        Text("Exams")
                        Spacer()
                        Button(action: {addExam()}, label: {
                            Image(systemName: "plus")
                        })
                    }
                }
                Section {
                    ForEach(course.roster) { student in
                        NavigationLink {
                            Text("\(student.first) \(student.last)")
                        } label: {
                            Text("\(student.first) \(student.last)")
                        }
                    }
                } header: {
                    HStack {
                        Text("Students")
                        Spacer()
                        Button(action: {addStudent()}, label: {
                            Image(systemName: "plus")
                        })
                    }
                }
            }
            .navigationTitle(course.name)
        }
    }
    
    private func addStudent() {
        let student = Student(first: String.randomString(), last: String.randomString())
        course.roster.append(student)
    }
    
    private func addExam() {
        let exam = Exam(name: String.randomString(), date: Date(), roster: course.roster)
        course.exams.append(exam)
    }
}


struct ExamView : View {
    @Bindable var course : Course
    @Bindable var exam : Exam
    
    init(course: Course, exam: Exam) {
        self.course = course
        self.exam = exam
    }
    
    var body : some View {
        Text(exam.name)
        ForEach(exam.grades) { grade in
            NavigationLink {
                Text("\(grade.student.first) \(grade.student.last)")
            } label: {
                Text("Exam: \(grade.student.first) \(grade.student.last)")
            }
        }
        
    }
}

#Preview {
    CourseListView()
        .modelContainer(for: Course.self, inMemory: true)
}

Run the above code and

  • add a course
  • select the course
  • add students
  • add an exam

This results in app termination with the following error:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: 'Illegal attempt to establish a relationship 'student' between objects in different contexts 
(source = <NSManagedObject: 0x600002217f70> 
    (entity: Grade; id: 0x6000002b90a0 <x-coredata:///Grade/tB22D0958-A107-4FA1-B705-CA54127FB53232>; data: {
    grade = nil;
    student = nil;
}) ,
 destination = <NSManagedObject: 0x6000021dd360> 
    (entity: Student; id: 0xa40ac2d6f4136136 <x-coredata://5F5F57E9-7355-46B0-9143-D08161C9FA59/Student/p3>; data: {
    first = cmzngpm;
    last = odzlkmv;
}))'

What is wrong with how the data is modeled and being used?

1

There are 1 answers

0
swity learner On

To fix this, I first added the inverse relationship between Student and Grades by updating Student as follows:

@Model
final class Student {
    var first : String
    var last : String
    
    @Relationship(inverse: \Grade.student)
    var grades : [Grade]
    
    init(first: String, last: String) {
        self.first = first
        self.last = last
        self.grades = []
    }
}

I updated Grade as follows, making Student optional

@Model
final class Grade {
    var student : Student?
    var grade : Int
    
    init(student: Student?, grade: Int) {
        self.student = student
        self.grade = grade
    }
}

I then updated the Exam class initializer as follows

init(name: String, date: Date, roster: [Student]) {
        self.name = name
        self.date = date
        self.grades = []
        
        roster.forEach( {
            let grade = Grade(student: nil, grade: 0)
            $0.grades.append(grade)
            grades.append(grade)
        } )
    }

and all is fixed. Here is the fully updated program:

import SwiftUI
import SwiftData

extension String {
    static func randomString(length: Int = 7) -> String {
        let letters = "abcdefghijklmnopqrstuvwxyz"
        return String((0..<length).map{ _ in letters.randomElement()! })
    }
}


@Model
final class Grade {
    var student : Student?
    var grade : Int
    
    init(student: Student?, grade: Int) {
        self.student = student
        self.grade = grade
    }
}

@Model
final class Exam {
    var name : String
    var date : Date
    var grades : [Grade]
    
    init(name: String, date: Date, roster: [Student]) {
        self.name = name
        self.date = date
        self.grades = []
        
        roster.forEach( {
            let grade = Grade(student: nil, grade: 0)
            $0.grades.append(grade)
            grades.append(grade)
        } )
    }
}

@Model
final class Student {
    var first : String
    var last : String
    
    @Relationship(inverse: \Grade.student)
    var grades : [Grade]
    
    init(first: String, last: String) {
        self.first = first
        self.last = last
        self.grades = []
    }
}


@Model
final class Course {
    var name : String
    var roster : [Student]
    var exams : [Exam]
    
    init(name: String, roster: [Student]) {
        self.name = name
        self.roster = roster
        self.exams = []
    }
}

@main
struct StudentsApp: App {
    var sharedModelContainer: ModelContainer = {
        let schema = Schema([Course.self, Student.self, Exam.self, Grade.self])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()

    var body: some Scene {
        WindowGroup {
            CourseListView()
        }
        .modelContainer(sharedModelContainer)
    }
}



struct CourseListView : View {
    @Environment(\.modelContext) private var modelContext
    @Query private var courses: [Course]
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(courses) { course in
                    NavigationLink {
                        CourseView(course: course)
                    } label: {
                        Text(course.name)
                    }
                }
            }
            .navigationTitle("Courses")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button(action: {addCourse()}, label: {
                        Image(systemName: "plus")
                    })
                }
            }
        }
    }
    
    private func addCourse() {
        modelContext.insert(Course(name: String.randomString(), roster: []))
    }
}

struct CourseView: View {
    @Environment(\.modelContext) private var modelContext
    @Bindable var course : Course

    var body: some View {
        NavigationStack {
            List {
                Section {
                    ForEach(course.exams) { exam in
                        NavigationLink {
                            ExamView(course: course, exam: exam)
                        } label: {
                            Text(exam.name)
                        }
                    }
                } header: {
                    HStack {
                        Text("Exams")
                        Spacer()
                        Button(action: {addExam()}, label: {
                            Image(systemName: "plus")
                        })
                    }
                }
                Section {
                    ForEach(course.roster) { student in
                        NavigationLink {
                            Text("\(student.first) \(student.last)")
                            List {
                                ForEach(student.grades) { grade in
                                    Text("\(grade.grade)")
                                }
                            }
                        } label: {
                            Text("\(student.first) \(student.last)")
                        }
                    }
                } header: {
                    HStack {
                        Text("Students")
                        Spacer()
                        Button(action: {addStudent()}, label: {
                            Image(systemName: "plus")
                        })
                    }
                }
            }
            .navigationTitle(course.name)
        }
    }
    
    private func addStudent() {
        let student = Student(first: String.randomString(), last: String.randomString())
        course.roster.append(student)
    }
    
    private func addExam() {
        let exam = Exam(name: String.randomString(), date: Date(), roster: course.roster)
        course.exams.append(exam)
    }
}


struct ExamView : View {
    @Bindable var course : Course
    @Bindable var exam : Exam
    
    init(course: Course, exam: Exam) {
        self.course = course
        self.exam = exam
    }
    
    var body : some View {
        Text("\(exam.name) has \(exam.grades.count) grades")
        List {
            ForEach(exam.grades) { grade in
                NavigationLink {
                    Text("\(grade.student!.first) \(grade.student!.last)")
                } label: {
                    Text("\(grade.student!.first) \(grade.student!.last)")
                    Spacer()
                    Text("\(grade.grade)")
                }
            }
        }
        
    }
}

#Preview {
    CourseListView()
        .modelContainer(for: Course.self, inMemory: true)
}