How to do a many to many/through relationship? #404

Open
opened 2025-12-29 15:31:01 +01:00 by adam · 1 comment
Owner

Originally created by @pejrich on GitHub (Feb 19, 2023).

Here is an example of how I would think to set this up.

class User: CoreStoreObject {
  @Field.Stored("username")
  var username: String = ""
  @Field.Relationship("postLikes", inverse: \.$user)
  var postLikes: [PostLike]
}

class PostLike: CoreStoreObject {
  @Field.Relationship("post")
  var post: Post?
  @Field.Relationship("user")
  var user: User?
}

class Post: CoreStoreObject {
  @Field.Stored("text")
  var text: String = ""
  @Field.Relationship("postLikes", inverse: \.$post)
  var postLikes: [PostLike]
  @Field.Virtual("likers", customGetter: { (object, field) in
    return object.$postLikes.value.map { $0.user }
  })
  var likers: [User]
}

But when I do this, the Field.Virtual closure gets the error Type of expression is ambiguous without more context. And if I set a variable to object.$postLikes.value, it shows <<type error>> rather than what i'd expect being [PostLike]. Am I doing something wrong?

Originally created by @pejrich on GitHub (Feb 19, 2023). Here is an example of how I would think to set this up. ``` class User: CoreStoreObject { @Field.Stored("username") var username: String = "" @Field.Relationship("postLikes", inverse: \.$user) var postLikes: [PostLike] } class PostLike: CoreStoreObject { @Field.Relationship("post") var post: Post? @Field.Relationship("user") var user: User? } class Post: CoreStoreObject { @Field.Stored("text") var text: String = "" @Field.Relationship("postLikes", inverse: \.$post) var postLikes: [PostLike] @Field.Virtual("likers", customGetter: { (object, field) in return object.$postLikes.value.map { $0.user } }) var likers: [User] } ``` But when I do this, the `Field.Virtual` closure gets the error `Type of expression is ambiguous without more context`. And if I set a variable to `object.$postLikes.value`, it shows `<<type error>>` rather than what i'd expect being `[PostLike]`. Am I doing something wrong?
Author
Owner

@JohnEstropia commented on GitHub (Feb 20, 2023):

That's not supposed to work, at the very least I did not design Virtual fields to be used for relationships. There is currently no subscript for ObjectProxy that allows relationship access:
Screen Shot 2023-02-20 at 9 56 21

The reason for this is because we will hit problems when working with ObjectSnapshots, which only copies Stored, Virtual, and Coded fields. Relationship access can't be thread-safe as they need to be fetched from particular contexts.

What I would suggest is to make likers a Relationship field and sync its value with postLikes whenever it gets updated. If that sounds like a maintenance burden, then an extension on Post should be sufficient as well:

extension Post {
    func likers() -> [User] {
        return self.postLikes.compactMap({ $0.user })
    }
}

extension ObjectSnapshot where O == Post {
    func likers() -> [ObjectSnapshot<User>]? { 
        // this cannot be access from a different context the ObjectSnapshot was created from
        return self.$postLikes?.compactMap({ $0.user.asSnapshot() })
    }
    func likers(in dataStack: DataStack) -> [ObjectSnapshot<User>]? {
        // note that if this object is already deleted, relationships cannot be accessed even if the fields are available within the snapshot, thus the Optional return value
        return self.asReadOnly(in: dataStack)?.postLikes.compactMap({ $0.user.asSnapshot(in: dataStack) })
    }
    func likers(in transaction: BaseDataTransaction) -> [ObjectSnapshot<User>]? {
        // Same here
        return self.asReadOnly(in: transaction)?.postLikes.compactMap({ $0.user.asSnapshot(in: transaction) })
    }
}

(I typed this by hand so it may not compile as-is, but I hope this gives you an idea)

Understandably, relationship-dependent computed fields are some of the use cases for Core Data's derived attributes and so far I haven't had support for it in CoreStore. But even then, Core Data's derived attributes support still only supports aggregated values such as sums and counts, and not collections of objects. If there's a large demand for it I'll try thinking of an elegant way to implement it, but so far extensions handle this use case well.

@JohnEstropia commented on GitHub (Feb 20, 2023): That's not supposed to work, at the very least I did not design `Virtual` fields to be used for relationships. There is currently no subscript for `ObjectProxy` that allows relationship access: <img width="583" alt="Screen Shot 2023-02-20 at 9 56 21" src="https://user-images.githubusercontent.com/3029684/219986751-93023330-5819-4d0c-9b07-93378e1cd9a2.png"> The reason for this is because we will hit problems when working with `ObjectSnapshot`s, which only copies `Stored`, `Virtual`, and `Coded` fields. Relationship access can't be thread-safe as they need to be fetched from particular contexts. What I would suggest is to make `likers` a `Relationship` field and sync its value with `postLikes` whenever it gets updated. If that sounds like a maintenance burden, then an extension on `Post` should be sufficient as well: ```swift extension Post { func likers() -> [User] { return self.postLikes.compactMap({ $0.user }) } } extension ObjectSnapshot where O == Post { func likers() -> [ObjectSnapshot<User>]? { // this cannot be access from a different context the ObjectSnapshot was created from return self.$postLikes?.compactMap({ $0.user.asSnapshot() }) } func likers(in dataStack: DataStack) -> [ObjectSnapshot<User>]? { // note that if this object is already deleted, relationships cannot be accessed even if the fields are available within the snapshot, thus the Optional return value return self.asReadOnly(in: dataStack)?.postLikes.compactMap({ $0.user.asSnapshot(in: dataStack) }) } func likers(in transaction: BaseDataTransaction) -> [ObjectSnapshot<User>]? { // Same here return self.asReadOnly(in: transaction)?.postLikes.compactMap({ $0.user.asSnapshot(in: transaction) }) } } ``` (I typed this by hand so it may not compile as-is, but I hope this gives you an idea) Understandably, relationship-dependent computed fields are some of the use cases for Core Data's [derived attributes](https://developer.apple.com/documentation/coredata/nsderivedattributedescription) and so far I haven't had support for it in CoreStore. But even then, Core Data's derived attributes support still only supports aggregated values such as sums and counts, and not collections of objects. If there's a large demand for it I'll try thinking of an elegant way to implement it, but so far extensions handle this use case well.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/CoreStore#404