在 UUID 类型属性上使用 @FetchRequest 谓词过滤进行不可靠更新?

Posted

技术标签:

【中文标题】在 UUID 类型属性上使用 @FetchRequest 谓词过滤进行不可靠更新?【英文标题】:Unreliable update with @FetchRequest predicate filtering on UUID typed attributes? 【发布时间】:2021-10-13 23:09:24 【问题描述】:

我是 Swift 的新手,并试图通过一些(我认为!)简单的项目来更好地理解 Core Data。

我正在尝试使用@FetchRequest 属性包装器来列出属于给定父项的子项。这适用于大多数情况,但当父级关系是唯一会导致在添加新子项后显示子项的情况时则不适用。

我面临的问题似乎可以归结为使用 FetchRequest 装饰器中的谓词对 UUID 值进行的任何过滤,而不会像其他属性那样更新。我构建的最小示例(带有编辑)仅显示了使用 UUID 作为谓词时的行为,并避免了任何关系复杂化。

我的问题是,是否有一种“好”的方法可以使这种行为(驱动 FetchRequest 正确更新视图的谓词中的 UUID)工作,或者我缺少的东西,比如将其中一个部分注册为观察项目,还是我在错误的时间触发了更新?

我制作了一个示例来说明我遇到的难题。 注意:我正在使用 iOS 14.4Xcode 12.4 我感谢这些不是最新的,但我的机器不支持 Catalina 之前的操作系统,所以我被困在这里,直到我买得起一个新的。

我的核心数据模型如下:

Item 实体有两个属性(uuidUUID 类型)和一个 Int16 属性(number)。

该应用有两个按钮:+ 用于添加具有随机 number 值和固定 UUID 为 254DC... 的新项目和 5 用于添加项目,number 值为 5(和相同的、固定的 UUID)。

我用来驱动这个列表过滤项目实体的谓词:

匹配硬编码的 UUID 值* 或number 属性为 5

问题是,当添加(使用 + 按钮)一个新实体时,它应该显示在这个视图中,因为它的 uuid 属性匹配,它确实没有出现。如果您退出应用程序并返回它,它确实会出现(这证明谓词确实“匹配”了 UUID 值)。

点击 5 后(现在显示两行数字 5,如预期的那样):

点击+两次后(视图不变,应该有两行显示):

如果我们退出应用然后重新打开它,添加的项目(本例中为 300 和 304)仅现在可见:

问题的不同表述:为什么这些更新在 UUID 上运行不可靠,但与 number(Int16 类型)属性完美匹配?

按预期工作:

删除项目实体按预期工作,无论它们是否由于 number == 5 或其 UUID 匹配而在列表中

注意事项:

我大量使用了viewContext.perform,因为使用它解决了另一个未显示更新的故障。 我已经(基于this SO question/answer)已经尝试将行的每个元素包装为 ObservedObject。这也是在评论中提出的。已更新示例代码以执行此操作,这对此行为没有影响。 (元素不在 FetchRequest 生成的 items 集合中,因此不要被此代码包装。) 到处都是try!。这只是为了使示例代码尽可能小。

查看代码

import SwiftUI
import CoreData

struct ItemList: View 
    @Environment(\.managedObjectContext) private var viewContext
   
    static let filterUUID = "254DC821-F654-4D45-BC94-CEEE12A428CB"
    
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.number, ascending: true)],
        predicate: NSPredicate(format: "number == %@ OR uuid == %@", NSNumber(5), filterUUID))
    var items : FetchedResults<Item>

    var body: some View 
        NavigationView 
            List 
                ForEach(items)  item in ItemRowView(item: item) 
                .onDelete(perform: deleteItems)
            
            .navigationTitle(Text(items.isEmpty ? "No items" : "Item count: \(items.count)"))
            .navigationBarTitleDisplayMode(.inline)
            .toolbar 
                ToolbarItemGroup(placement: .navigationBarTrailing) 
                    Button(action: addRandom) 
                        Label("Add Item", systemImage: "plus")
                    
                    
                    Button(action: addFive) 
                        Label("Add Five", systemImage: "5.circle.fill")
                    
                
            
        
    

    private func addItem(number: Int) 
        viewContext.perform 
            let newItem = Item(context: viewContext)
            newItem.uuid = UUID(uuidString: ItemList.filterUUID)
            newItem.number = Int16(number)
            try! viewContext.save()
        
    
    
    private func addRandom()  addItem(number: Int.random(in: 10...500)) 
    private func addFive()  addItem(number: 5) 

    private func deleteItems(offsets: IndexSet) 
            viewContext.perform 
                offsets.map  items[$0] .forEach(viewContext.delete)
                try! viewContext.save()
            
    



struct ItemRowView : View 
    @ObservedObject var item : Item
    
    var body : some View 
        HStack 
            Text("\(item.number)")
                .font(.largeTitle)
            Text(item.uuid?.uuidString.prefix(8) ?? "None")
        
    


我已经尝试了什么?

我花了两天时间搜索文档、*** 和各种博客,虽然有几件事乍一看似乎正是我正在寻找的答案,但我得到的示例越简单,它变得越混乱。

这开始是与父子获取相关的类似问题,但我已将我面临的实际问题缩小到更小的问题,这可以通过具有 UUID 且没有关系的单个实体来演示。


支持代码(App 和 PersistenceController)

import SwiftUI
import CoreData

@main
struct minimalApp: App 
    let persistenceController = PersistenceController.shared

    var body: some Scene 
        WindowGroup 
            ParentListView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        
    


struct PersistenceController 
    static let shared = PersistenceController()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) 
        container = NSPersistentContainer(name: "minimal")
        container.loadPersistentStores(completionHandler:  (storeDescription, error) in
            if let error = error as NSError? 
                fatalError("Unresolved error \(error), \(error.userInfo)")
            
        )
    

【问题讨论】:

每个孩子也需要被包裹在一个@ObservedObject 中。只需使用从 ForEach 中获取项目的子内容创建一个行子视图。此外,如果您正确附加子项,则不需要子项获取请求,只需使用您已经拥有的被观察父项中的子项。 要解决集合问题,只需将集合包装在 Array(parentItem.children) 中 @loremipsum 感谢 cmets。已经尝试过 @ObservedObject 包装(为此添加了注释和源链接),其中(可以理解,因为有问题的项目不在那里包装)在这里不起作用。已更新问题,重点关注基于 UUID 的过滤问题,避免实体关系分散注意力的复杂性。 【参考方案1】:

我认为奇怪的行为是由于您的谓词造成的,该谓词正在以两种不同的方式进行评估。当您第一次运行应用程序时,或者在关闭并重新启动后,谓词会被解析并传递给 SQLite(作为 SELECT 语句中的 WHERE 子句)。此后,当您添加新项目时,谓词会直接在内存中进行评估(即在 NSManagedObjectContext 中)——无需涉及 SQLite。

在您的谓词中,您将 UUID 类型属性与字符串值进行比较 - 失败。即使 UUID 属性的字符串表示形式与您与之比较的字符串相同,上下文也会将它们视为不同,并将新对象视为未通过谓词。因此视图没有更新。

但是,SQLite 对类型不匹配的容忍度更高。 (我猜 CoreData 的 UUID 属性的实现是存储字符串表示 - 但这只是一个猜测)。因此,当您退出并重新启动时,提取由 SQLite 处理,它将新对象视为满足谓词并相应地将其包含在结果中。

为了获得正确的行为,我认为您需要您的谓词将 UUID 属性与具有正确字符串表示的 UUID 进行比较:

NSPredicate(format: "number == %@ OR uuid == %@", NSNumber(5), UUID(uuidString: ItemList.filterUUID))

【讨论】:

是的!就是这样。这解决了我看到的问题,此外,它首先不起作用的原因是有道理的。感谢您的详细解释和帮助。有一点,它必须是NSUUID(uuidString: filterUUID)! 当使用非NS UUID 时,编译器抱怨该类型不是有效的CVarArg;我猜传递给 NSPredicates 的项目需要是 NSObject 的子类。

以上是关于在 UUID 类型属性上使用 @FetchRequest 谓词过滤进行不可靠更新?的主要内容,如果未能解决你的问题,请参考以下文章

AttributeError:“UUID”对象在使用与后端无关的 GUID 类型时没有属性“replace”

是否始终将 UUID 用于 JPA 实体的 id 属性并在其上使用等于和哈希码?

(转)blkid命令 获取文件系统类型UUID

错误“字符串”类型中不存在属性“getItem”.ts(2339)

使用laravel从sql server生成UUID数据类型(uniqueidentifier)失败

sequelize(和 sequelize-cli)queryInterface.createTable 在 PostgreSQL 上,PK id 类型为 UUID 和 defaultValue:Se