Search code examples
swiftswiftdata

Update one-to-one relationship in SwiftData


I'm trying build a model where you can add shows to your watchlist and review them. When you either add a show to your watchlist or review it, I'm creating a new show in the db if it doesn't already exists, otherwise i retrieve it and update it. My issue is that when i'm retrieving an existing show and inserting the new watchlist item or review to the db, it raises the error "Illegal attempt to establish a relationship between objects in different contexts". From what i understood, it's because the watchlist item or the review don't already exists while the show does. But i don't know how to work around this behavior. I've tried to create a new show every time but when i'm inserting it through the relation of either watchlist item or review, it always creates a new show instead of updating if there's an existing one. Here's my implementation, thanks for your help

@Model
final class LocalWatchlistItem {
    var addedAt: Date
    var show: LocalShow

    init(show: LocalShow, addedAt: Date = .now) {
        self.show = show
        self.addedAt = addedAt
    }
}
@Model
final class LocalReview {
    var show: LocalShow
    ...

    init(show: LocalShow, ...) {
        self.show = show
        ...
    }
}

@Model
final class LocalShow: Showable {
    @Attribute(.unique) var id: String
    ...
    @Relationship(deleteRule: .cascade, inverse: \LocalWatchlistItem.show) var watchlistItem: LocalWatchlistItem? = nil
    @Relationship(deleteRule: .cascade, inverse: \LocalReview.show) var reviews = [LocalReview]()

    init(
        id: String,
        ...
    ) {
        self.id = id
        ...
    }
func upsertShow(_ show: Show) throws -> LocalShow {
        if let existingShow = try getShow(show.key) {
            existingShow.updatedAt = .now
            existingShow.title = show.title
            ...
            return existingShow
        } else {
            return LocalShow(
                id: show.key,
                createdAt: .now,
                updatedAt: nil,
                title: show.title,
                ...
            )
        }
    }
func addToWatchlist(_ show: Show) throws -> LocalWatchlistItem {
        let upsertedShow = try upsertShow(show)
        let watchlistItem = LocalWatchlistItem(show: upsertedShow)
        database.insert(watchlistItem)
        return watchlistItem
    }
func createReview(for show: Show, rating: Int, comment: String?) throws -> LocalReview {
        let upsertedShow = try upsertShow(show)
        let review = LocalReview(show: upsertedShow, rating: rating, comment: comment)
        database.insert(review)
        return review
    }

Solution

  • I've found a workaround by making 'show' optional on both 'LocalWatchlistItem' and 'LocalReview' and by modifying my functions as follows:

    func addToWatchlist(_ show: Show) throws -> LocalWatchlistItem {
            let upsertedShow = try upsertShow(show)
            let watchlistItem = LocalWatchlistItem()
            database.insert(watchlistItem)
            watchlistItem.show = upsertedShow
            return watchlistItem
        }
    
    func createReview(for show: Show, rating: Int, comment: String?) throws -> LocalReview {
            let upsertedShow = try upsertShow(show)
            let review = LocalReview(rating: rating, comment: comment)
            database.insert(review)
            review.show = upsertedShow
            return review
        }
    

    The logic is to insert or update the show depending on whether it exists or not, insert the 'watchlistItem' without any relation, and finally update the relations once both models are in the modelContext. With this approach, I ensure that I never have duplicates of 'LocalShow' and do not raise the error "Illegal attempt to establish a relationship between objects in different contexts" because both objects are correctly inserted.

    In general, when working with any kind of relations in SwiftData, I would now recommend always FIRST inserting models or retrieving them, THEN updating the relations. The only downside is the need for optionals on relations.