fatbobman / blogComments

1 stars 0 forks source link

揭秘 SwiftData 的数据建模原理 | 肘子的Swift记事本 #203

Open fatbobman opened 1 year ago

fatbobman commented 1 year ago

https://www.fatbobman.com/posts/unveiling-the-Data-Modeling-Principles-of-SwiftData/

在 SwiftData 的数项改进中,用纯代码声明数据模型无疑给 Core Data 开发者留下了深刻印象。本文将深入探讨 SwiftData 是如何通过代码创建数据模型的,使用了哪些新的语言特性,并展示了如何通过声明代码来创建 PersistentModel 实例。

mcmay commented 11 months ago

Thanks for sharing this in-depth insight into SwiftData. As you mentioned in the latter half of your article "Using this method directly will result in inconsistency between the data of the underlying NSManagedObject and the data of the surface-level PersistentModel." I tested it that's surely true. However,

I also discovered that assigning to a relationship property of an object causes the same inconsistency between the data of the underlying NSManagedObject and the data of the surface-level PersistentModel. For example, I have a class named 'StudentGroup' for example. This class is marked with the @Model macro for persistence. This class has a to-many relationship property named 'students' which is an array of objects of the 'Student' class. The 'student' property has an inverse of \Student.studentGroup in the @Relationship. Naturally, the 'Student' class has a 'studentGroup' to-one relationship property of the 'StudentGroup' class. The 'studentGroup' property is declared as an Optional, i.e. studengGroup: StudentGroup?.

Now I have an instance named 'stuGrp' of the 'StudentGroup' class and an array named 'myStudents' of Student object. When I tried to do stuG.students.append(contentsOf: myStudents), a runtime error pops up such as Exception NSException * "Unacceptable type of value for to-one relationship: property = \"studentGroup\"; desired type = NSManagedObject; given type = NSManagedObject; value = <NSManagedObject: 0x60000213caa0> (entity: StudentGroup; id: 0xbd34b428aed3c229 <x-coredata://39C3D710-4A53-41EE-A7B8-93079012C174/StudentGroup/p128>; data: {\n additionalInfo = \"\";\n classSchedule = \"{length = 50, bytes = 0x7b224d6f 6e646179 223a302c 22546875 ... 69646179 223a302c }\";\n classColor = \"UIExtendedSRGBColorSpace 0 0 0 0\";\n name = \"Class 1\";\n startTime = \"2023-12-23 22:00:00 +0000\";\n students = (\n \"0xbd34b428b8f3c239 <x-coredata://39C3D710-4A53-41EE-A7B8-93079012C174/Student/p49>\"\n );\n})." 0x0000600000d48780

I think that's a representation of the consistency mentioned in your article. However, I haven't been able to resolve this inconsistency so far. I'd be more than pleased to receive suggestions from you. Thank you so much for your help.

fatbobman commented 11 months ago

@mcmay

I don't think there will be any problems with such an operation. I created the following code as you described and it runs without any problems.

@Model
final class Item {
    var timestamp: Date
    @Relationship(deleteRule: .cascade)
    var tags:[Tag] = []

    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

@Model
final class Tag {
    var name:String
    @Relationship(deleteRule: .nullify,inverse: \Item.tags)
    var item:Item?
    init(name: String) {
        self.name = name
    }
}

add tags

let tag1 = Tag(name: "tag1")
let tag2 = Tag(name: "tag2")
items.first!.tags.append(contentsOf: [tag1,tag2])
try? modelContext.save()
print(items.first!.tags.map{$0.name})
mcmay commented 11 months ago

Thanks, @fatbobman, for your immediate response. The way I defined the two classes StudentGroup and Studentis basically the same as yours just with different properties. The properties in the StudentGroup property include some non-primitive types, though, like UIColor and a Dictionary. In addition, the StudentGroup has a MigrationPlan which includes 4 migration stages. Maybe it's due to those complexities of the class setup that the runtime error occurred. I have no idea how I can fix that. Maybe I'll just find some workarounds. Thank you again for your kind help.

mcmay commented 11 months ago

I read this article again for its importance in helping better understand how data is modeled in SwiftData. Everything in the expanded macro @Model is clearly explained in due detail except the '_SwiftDataNoType' struct. What is it? Why is it there? I look forward to seeing answers to those questions and potentially more discussions beyond that in one of your future articles. If there is already such a discussion included in your post collection or anywhere else, please provide a link reference thereto. Thanks heaps!

fatbobman commented 11 months ago

I read this article again for its importance in helping better understand how data is modeled in SwiftData. Everything in the expanded macro @model is clearly explained in due detail except the '_SwiftDataNoType' struct. What is it? Why is it there? I look forward to seeing answers to those questions and potentially more discussions beyond that in one of your future articles. If there is already such a discussion included in your post collection or anywhere else, please provide a link reference thereto. Thanks heaps!

I also haven't figured out the specific purpose of _SwiftDataNoType. In practical use, we can declare a type without _SwiftDataNoType and use it normally without utilizing the @Model macro. Since the first beta version, the code generated by the @Model macro in SwiftData has been constantly evolving. Perhaps _SwiftDataNoType is being prepared for the future, or perhaps it has already been deprecated.

mcmay commented 11 months ago

@fatbobman

I read this article again for its importance in helping better understand how data is modeled in SwiftData. Everything in the expanded macro @model is clearly explained in due detail except the '_SwiftDataNoType' struct. What is it? Why is it there? I look forward to seeing answers to those questions and potentially more discussions beyond that in one of your future articles. If there is already such a discussion included in your post collection or anywhere else, please provide a link reference thereto. Thanks heaps!

I also haven't figured out the specific purpose of _SwiftDataNoType. In practical use, we can declare a type without _SwiftDataNoType and use it normally without utilizing the @Model macro. Since the first beta version, the code generated by the @Model macro in SwiftData has been constantly evolving. Perhaps _SwiftDataNoType is being prepared for the future, or perhaps it has already been deprecated.

Thanks for your immediate response. I guess _SwiftDataNoType may be used as a type erasing placeholder via the assignment statement _timeStamp = _SwiftDataNoType(). _timeStamp is likely to be the name of the property of the backingData which is an NSManagedObject. It naturally maps to the timeStamp property stored in memory. Values in stored as backingData may not have type information accompanying them in the underlying SQLite database which has very limited allowed storage types. The type information of the backingData values may be stored in instances of SwiftData.Schema.PropertyMetadata which may be independently stored as a separate reference table in the same database. When a value needs to be fetched from the database, the value and its corresponding type information are brought into memory via the encode(to:) method provided by the Encodable protocol. I have not seen the implementation, so it's just an assumption.

fatbobman commented 11 months ago

Thanks for your immediate response. I guess _SwiftDataNoType may be used as a type erasing placeholder via the assignment statement _timeStamp = _SwiftDataNoType(). _timeStamp is likely to be the name of the property of the backingData which is an NSManagedObject. It naturally maps to the timeStamp property stored in memory. Values in stored as backingData may not have type information accompanying them in the underlying SQLite database which has very limited allowed storage types. The type information of the backingData values may be stored in instances of SwiftData.Schema.PropertyMetadata which may be independently stored as a separate reference table in the same database. When a value needs to be fetched from the database, the value and its corresponding type information are brought into memory via the encode(to:) method provided by the Encodable protocol. I have not seen the implementation, so it's just an assumption.

There is indeed a possibility.