fatbobman / blogComments

1 stars 0 forks source link

Relationships in SwiftData: Changes and Considerations #218

Open fatbobman opened 7 months ago

fatbobman commented 7 months ago

Relationships in SwiftData: Changes and Considerations

In previous two articles, Mastering Relationships in Core Data: Fundamentals and Mastering Relationships in Core Data: Practical Application, we explored in detail the concepts and techniques of relationships in Core Data. While much of this knowledge is also applicable to SwiftData, Core Data's successor, SwiftData introduces several significant changes in handling relationships. This article focuses on the changes that have occurred in the aspect of relationships within SwiftData, as well as the potential challenges and noteworthy details arising from these changes.

SwiftData 中的关系:变化与注意事项

在之前的两篇文章 掌握 Core Data 中的关系:基础掌握 Core Data 中的关系:实战 中,我们详细探讨了 Core Data 中关系的概念和技巧。虽然这些知识在很大程度上同样适用于 Core Data 的继任者 SwiftData,但 SwiftData 在关系处理上也引入了一些显著的变化。本文将重点介绍 SwiftData 在关系方面发生的变化,以及由此带来的潜在挑战和值得注意的细节。

Shinolr commented 7 months ago

One-to-Many(一对多):两端都是 Optional 时 SwiftData 会自动设置逆向关系;一端不为 Optional 时,需要开发者明确设置逆向关系

这里也许有特例,当relationship包含@Attribute(.unique)的属性时,即使两端都是Optional,也要手动添加@Relationship(inverse: \Bar.foo)。不然会报错:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Unable to resolve conflict: relationship "bars" (on entity "Foo") does not have an inverse

一对一包含unique属性时没问题、多对多的我还没测试过。

可能的原因可以参考下这个回答和这个问题

fatbobman commented 7 months ago

One-to-Many(一对多):两端都是 Optional 时 SwiftData 会自动设置逆向关系;一端不为 Optional 时,需要开发者明确设置逆向关系

这里也许有特例,当relationship包含@Attribute(.unique)的属性时,即使两端都是Optional,也要手动添加@Relationship(inverse: \Bar.foo)。不然会报错:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Unable to resolve conflict: relationship "bars" (on entity "Foo") does not have an

@Shinolr 能提供一个问题的简单模型吗? 我尝试按照你提供的 link,创建了如下模型,并没有问题。

@Model
class Person {
    var name:String
    var department:Department?
    init(name: String) {
        self.name = name
    }
}

@Model
class Department {
    @Attribute(.unique)
    var title:String
    var persons:[Person]?
    init(title:String) {
        self.title = title
    }
}
Shinolr commented 6 months ago

花了些时间复现这个问题,我刚才举的例子简单了,只有两个互为relationship的实体,并不能复现出错误。

@Model
final class Department {
  @Attribute(.unique)
  var id: Int
  var name: String

  // @Relationship(inverse: \Employee.department)
  var employees: [Employee]?
  var info: Info?

  init(id: Int, name: String) {
    self.id = id
    self.name = name
    self.info = Info(id: id)
  }
}

@Model
final class Employee {
  @Attribute(.unique)
  var id: Int
  var name: String

  var department: Department?
  var info: Info?

  init(id: Int, name: String) {
    self.id = id
    self.name = name
    self.info = Info(id: id)
  }
}

@Model
final class Info {
  @Attribute(.unique)
  var id: Int

  var department: Department?
  var employee: Employee?

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

显式的指定DepartmentEmployee的relationship后,功能正常,见main

注释掉relationship那行代码后,再次插入已经存储的、具有相同id的employee后,save时则会抛出异常(点两次加号):

Error Domain=NSCocoaErrorDomain Code=1570 "%{PROPERTY}@ is a required value." UserInfo={NSValidationErrorObject=<NSManagedObject: 0x600001508690> (entity: Info; id: 0xba4745033f81d10e <x-coredata://D66625A2-7636-465A-9482-09D4BA108995/Info/p3>; data: { department = nil; employee = nil; id = nil; }), NSLocalizedDescription=%{PROPERTY}@ is a required value., NSValidationErrorKey=id, NSValidationErrorValue=null}

我用可视化工具,查看存储的数据时,发现employee->info是有值的,反过来info->employee却是null。实际上第一次存employeeinfo->employee就是null,见implicitly_relationship

SCR-20240126-tjfp

第一行是info->department,第二行是info->employee

如果删除Info有关的全部代码,employee->infoinfo->employee就又都是正常的了。Demo里,DepartmentEmployee的存在完全是类似的,唯一的区别只有Department->Employee是一对多,而Employee->Department是多对一,Department的存储却是一直正常的。

fatbobman commented 6 months ago

@Shinolr,非常感谢你分享的示例项目。 实际上,这个问题可以归结为 SwiftData 在将模型代码转换为 NSManagedObjectModel 时的处理能力。当面对较为复杂的模型时,SwiftData 显得能力有限,导致了这一问题的出现。 在你的项目中,模型代码对应的 NSManagedObjectModel , Info 和 Employee 之间没有设置正确的逆向关系( 注释掉 Relationship 的逆向设置)。 而且,这并不受是否有 Attribute(.unique) 的影响。 为了解决这个问题,比较推荐的方式是在 Info 和 Employee 之间设置显示的逆向关系。 至于,为什么给 var employees: [Employee]? 设置可以好用,我就不得而知了。这也再次证明,SwiftData 在转换模型时,会出现一些问题。

Shinolr commented 6 months ago

感觉你的回答。你说的对,更合理的原因确实应该是当面对较为复杂的模型时,SwiftData 显得能力有限,导致了这一问题的出现。我之所以猜测与@Attribute(.unique)有关,是因为项目中另一个异常,而解决这个异常也是需要显式的指定relationship的inverse关系移除relationship里@Attribute(.unique)相关的代码。

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Unable to resolve conflict: relationship "episodeItems" (on entity "WatchedSeasonItem") does not have an inverse'

但是很遗憾,我没办法在demo中复现出这个异常,因为项目中的模型结构更加复杂,且有更多层次的relationship关系。当然这也印证了你的解释,SwiftData转换复杂模型时能力有限。

另外,想请教肘大一个问题,就是@ModelActor的正确使用姿势。

demo里的用法,是在SwiftDataStack里实例化@ModelActor修饰的Handler,然后SwiftDataStack作为依赖注入到Store里,Store最后在SwiftUI被发起调用。

我的问题是,如果在Handler里使用modelContext,则之后的每一个调用环节都是使用awaitPersistentModel传递过程中就不可避免的要跨越线程。如果Handler里使用modelContainer.mainContext,为了保证PersistentModel始终在主线程传递,那么每个调用方法都需要标记@MainActor,这种方式取的过程还可以接受,但存的过程又没必要在主线程实施。

我目前在项目里的解决方案非常麻烦,我为每个模型都创建了两份,一份是@Modelclass类型,一份是struct类型。并且为两种类型提供了互相转换的协议,PersistentModelConvertibleValueModelConvertible,并为每个对应的模型实现asPersistentModelasValueModel方法,从而支持二者互相转换。存取时使用classPersistentModel,传递时使用structValueModel

请问PersistentModel跨越线程是安全的吗(我实际的使用过程跨越了线程好像没什么问题)?是否应该使用modelContainer.mainContext?这种情况的正确解决方案应该是什么?

  // MainContext
  @MainActor
  func retrieve<T>(
    predicate: Predicate<T>? = nil,
    sortBy sortDescriptors: [SortDescriptor<T>] = []
  ) throws -> [T] where T: PersistentModel {
    let fetchDescriptor = FetchDescriptor(
      predicate: predicate,
      sortBy: sortDescriptors
    )
    return try modelContainer.mainContext.fetch(fetchDescriptor)
  }

  // ModelContext
  func _retrieve<T>(
    predicate: Predicate<T>? = nil,
    sortBy sortDescriptors: [SortDescriptor<T>] = []
  ) throws -> [T] where T: PersistentModel {
    let fetchDescriptor = FetchDescriptor(
      predicate: predicate,
      sortBy: sortDescriptors
    )
    return try modelContext.fetch(fetchDescriptor)
  }

两种模型

protocol PersistentModelConvertible<Model> {
  associatedtype Model: PersistentModel
  var asPersistentModel: Model { get }
}

protocol ValueModelConvertible<Model> {
  associatedtype Model
  var asValueModel: Model { get }
}

@Model
class Foo: ValueModelConvertible {
  var name: String
  init(name: String) {
    self.name = name
  }

  var asValueModel: Bar {
    .init(name: name)
  }
}

struct Bar: PersistentModelConvertible {
  var name: String

  var asPersistentModel: Foo {
    .init(name: name)
  }
}
fatbobman commented 6 months ago

@Shinolr 对于 SwiftData , 我会放弃之前通过 struct 进行转换这一过程,因为这样就失去了 SwiftData 的一大优势了。 PersistentModel 并不能跨线程( 确切来说是不能跨上下文),但是可以通过传递 PersistentModelIdentifier 的方式实现在不同 Actor 之间的传递。 然后通过下标方法快速获取对应的数据。 如果可能,我还是会建议你从在视图中使用 Query 来获取数据,而写操作通过 Actor 来进行。 这样就无需担心 MainActor 污染问题了。另外 Query 的实现与NSFetchedResultsController 相同,它只会在首次获取数据时产生 IO 操作。之后不会。效率要比你当前每次写操作后,重新读取高效的多。 下周的文章会讲到这个部分。

image

Query 的原理与 FetchRequest 一样。

wisepmlin commented 4 months ago

CoreData重构,使用SwiftData后,出现这个 debug,真机调试时候占用 170% 的 cpu 一个线程?这种大佬遇见过吗? Shuto(3088,0x1eaee7f00) malloc: Unable to set up reclaim buffer (46) - disabling large cache