Search code examples
iosswiftuiswiftdata

SwiftData crash "Illegal attempt to establish a relationship"


I am using SwiftData and SwiftUI, xcode 15 beta 5. I am trying to establish relationships between entities in DB. The is a User, a Book, and an Ownership between them.

struct asdApp: App {

    let container = try! ModelContainer(for: [Ownership.self, User.self, Book.self])

    var body: some Scene {
        WindowGroup {
            ContentView()
                .task {
                    let jon = User()
                    container.mainContext.insert(jon)
                    let book = Book()
                    container.mainContext.insert(book)
                    let o = Ownership(keeper: jon, book: book) // <-- crash here!!
                    container.mainContext.insert(o)
                }
        }
        .modelContext(container.mainContext)
    }
}

Here are the models, so that you have the full code:

@Model
final class User {
    @Attribute(.unique) let id: UUID

    init(id: UUID = UUID()) {
        self.id = id
    }
}

@Model
final class Book {
    @Attribute(.unique) let id: UUID

    init(id: UUID = UUID()) {
        self.id = id
    }
}

@Model
final class Ownership {
    @Attribute(.unique) var id: UUID
    var keeper: User
    var book: Book

    init(id: UUID = UUID(), keeper: User, book: Book) {
        self.id = id
        self.keeper = keeper
        self.book = book
    }
}

Here is the full error:

Thread 1: "Illegal attempt to establish a relationship 'keeper' between objects in different contexts (source = <NSManagedObject: 0x60000212e5d0> (entity: Ownership; id: 0x600000243560 <x-coredata:///Ownership/tFADECEE1-82F7-4071-93B1-259354AE2E2A4>; data: {\n    book = nil;\n    id = \"6A63596F-66C2-45B3-91C0-FF24FF88B1B5\";\n    keeper = nil;\n}) , destination = <NSManagedObject: 0x60000214fed0> (entity: User; id: 0x600000299180 <x-coredata:///User/tFADECEE1-82F7-4071-93B1-259354AE2E2A2>; data: {\n    id = \"59B5122C-5CC6-4105-8F11-D48B6D46E63D\";\n}))"

First question: There is only one context, so why the crash?

There is no error if I change the order like this:

var body: some Scene {
    WindowGroup {
        ContentView()
            .task {
                let jon = User()
                let book = Book()
                let o = Ownership(keeper: jon, book: book)
                container.mainContext.insert(jon)
                container.mainContext.insert(book)
                container.mainContext.insert(o)
            }
    }
    .modelContext(container.mainContext)
}

But it doesn't work right, since it creates an extra book. Here is how I found out:

@main
struct asdApp: App {

    let container = try! ModelContainer(for: [Ownership.self, User.self, Book.self])

    var body: some Scene {
        WindowGroup {
            ContentView()
                .task {
                    clearAll(modelContainer: container)
                    let context = container.mainContext

                    let jon = User()
                    let book = Book()
                    let o = Ownership(keeper: jon, book: book)
                    context.insert(jon)
                    context.insert(book)
                    context.insert(o)

                    do {
                        for (index, user) in try  context.fetch(FetchDescriptor<User>()).enumerated() {
                            print("\(index) [USER]: id: \(user.id)")
                        }
                        
                        for (index, user) in try  context.fetch(FetchDescriptor<Book>()).enumerated() {
                            print("\(index) [BOOK]: id: \(user.id)")
                        }

                        for (index, user) in try  context.fetch(FetchDescriptor<Ownership>()).enumerated() {
                            print("\(index) [OWN]: id: \(user.id)")
                        }
                    } catch {
                        fatalError("Error while fetching all entities: \(error)")
                    }
                }
        }
        .modelContext(container.mainContext)
    }

    @MainActor
    func clearAll(modelContainer: ModelContainer) {
        do {
            (try modelContainer.mainContext.fetch(FetchDescriptor<User>()))
                .forEach {
                    modelContainer.mainContext.delete($0)
                }

            (try modelContainer.mainContext.fetch(FetchDescriptor<Book>()))
                .forEach {
                    modelContainer.mainContext.delete($0)
                }

            (try modelContainer.mainContext.fetch(FetchDescriptor<Ownership>()))
                .forEach {
                    modelContainer.mainContext.delete($0)
                }

            try modelContainer.mainContext.save()
        } catch {
            fatalError("Error while fetching all entities: \(error)")
        }
    }
}

Here is the output:

0 [USER]: id: 14B69B68-1123-4658-9399-7E60E1051A2D
0 [BOOK]: id: 3A52F512-E82F-4574-B2EF-A0B9258EA094
1 [BOOK]: id: 99FC93F5-B81F-4060-A549-869472E53D1D
0 [OWN]: id: 7F1D089A-AF00-4AB0-A17A-275E8448FC20

Second question: Why 2 books, and how did they get different ids, if I only call init once (during debug it only comes there once)? I want 1 book, 1 user and 1 relationship.

Actual example is more complicated, this is the bare minimum I could achieve.


Solution

  • Here is the working solution from @Paulw11 (unfortunately it is just posted as a comment):

    Properties in your model that are a relationship to another entity need the @Relationship decorator. You don't really need the Ownership class. A book can just have a keeper

    So, just use @Relationship, and you can indeed just store the owner in the book