Core Data结合CloudKit去重后Book仅保留首个关联Author问题
问题根因
核心bug出在对Core Data双向关系的实时可变集合做遍历时直接修改集合,导致枚举过程跳过元素,最终丢失作者关联。
Core Data的双向关系会自动同步两边的变更:当你从Author.books集合里删掉待删除的重复Book时,Core Data会自动把这个Author从反向的Book.authors集合里移除。你现在的代码直接遍历book.authors这个会实时变动的可变集合,实际执行流程如下:
- 某本待删除的重复Book原本关联了3个作者,集合为
[authorA, authorB, authorC] - for-in遍历第一个元素authorA,执行关联转移:把authorA的books里的重复Book删掉,加上保留的winner Book。这一步触发双向同步,authorA被自动从当前重复Book的authors集合里移除,集合变成
[authorB, authorC] - for-in的内部索引往后移一位,原本应该取索引1位置的authorB,但此时索引1对应的元素已经变成authorC,authorB被直接跳过,完全没有执行关联到winner Book的逻辑
- 最终只有第一个被遍历到的作者能正确关联到保留的Book,其余作者的关联全部丢失。
这个遍历实时集合的问题在作者去重、出版社去重函数里也存在,只是因为Book和出版社是多对一关系,单本书仅关联一个出版社,不会出现跳元素的情况,所以问题没有暴露。
修复方案
所有遍历Core Data关系集合的场景,先把集合转为不可变的本地快照再遍历,不要直接遍历实时更新的关系集合,避免枚举过程中集合变动导致元素被跳过。
修正后的代码如下:
private func remove(duplicatedBooks: [Book], winner: Book, performingContext: NSManagedObjectContext) { for book in duplicatedBooks { // 拷贝当前关联为不可变快照,避免遍历过程中集合被双向同步修改 guard let authors = book.authors?.allObjects as? [Author] else { // 无作者关联时直接处理出版社 if let publisher = book.publisher, let publisherBooks = publisher.books as? NSMutableSet { publisherBooks.remove(book) if !publisherBooks.contains(winner) { publisherBooks.add(winner) } } performingContext.delete(book) continue } for author in authors { guard let authorBooks = author.books as? NSMutableSet else { continue } if authorBooks.contains(book) { authorBooks.remove(book) // 避免重复添加已存在的关联,减少无意义的变更 if !authorBooks.contains(winner) { authorBooks.add(winner) } } } // 处理出版社关联 if let publisher = book.publisher, let publisherBooks = publisher.books as? NSMutableSet { if publisherBooks.contains(book) { publisherBooks.remove(book) if !publisherBooks.contains(winner) { publisherBooks.add(winner) } } } performingContext.delete(book) } } private func remove(duplicatedAuthors: [Author], winner: Author, performingContext: NSManagedObjectContext) { for author in duplicatedAuthors { // 拷贝关联快照 guard let books = author.books?.allObjects as? [Book] else { performingContext.delete(author) continue } for book in books { guard let bookAuthors = book.authors as? NSMutableSet else { continue } if bookAuthors.contains(author) { bookAuthors.remove(author) if !bookAuthors.contains(winner) { bookAuthors.add(winner) } } } performingContext.delete(author) } } private func remove(duplicatedPublishers: [Pub], winner: Pub, performingContext: NSManagedObjectContext) { for pub in duplicatedPublishers { // 拷贝关联快照 guard let books = pub.books?.allObjects as? [Book] else { performingContext.delete(pub) continue } for book in books { book.publisher = winner } performingContext.delete(pub) } }
补充说明:
- 用
allObjects将NSSet转为Swift数组,得到的是集合当前状态的不可变副本,遍历过程中不会被Core Data的双向同步逻辑修改,彻底避免枚举跳元素的问题 - 新增了关联存在性判断,避免向集合中添加已经存在的对象,减少不必要的NSManagedObjectContext变更通知,提升性能
- Book与Publisher是多对一关系,直接给
book.publisher赋值时,Core Data会自动维护反向的publisher.books集合,不需要手动操作集合,原有逻辑本身正确,只需要补全集合快照拷贝即可。
内容的提问来源于stack exchange,提问作者wildcard
相关产品推荐
相关产品推荐

