NSFetchRequest查询结果first始终返回实体,空存储触发重复异常
问题描述
我编写了一段Swift代码,本该返回nil的场景却始终返回Member实例。无论输入什么username,哪怕多次从模拟器删除应用,create函数总会触发throw抛出UserExists异常。
MemberViewModel代码
class MemberViewModel: ObservableObject { ... init(_ entity: Member? = nil) { self.entity = entity } init(name: String, username: String, password: String, age: String?, role: String, admin: Bool, imageName: String, imageTint: String) { let context = Self.container.viewContext self.entity = Member(context: context) self.name = name self.username = username self.password = password self.age = age ?? "" self.role = role self.admin = admin self.imageName = imageName self.imageTint = imageTint } func create() throws -> Void { let context = Self.container.viewContext if entity!.username != nil { if try Self.fetch(username: entity!.username!) != nil { throw MemberError.UserExists(username: entity!.username!) } if context.hasChanges { try context.save() } } } static private func fetch(username: String) throws -> Member? { let fetchRequest: NSFetchRequest<Member> = Member.fetchRequest() let context = Self.container.viewContext fetchRequest.predicate = NSPredicate(format: "username[c] == %@", username) return try? context.fetch(fetchRequest).first } ... } extension MemberViewModel { public enum MemberError: Error { case UserExists(username: String) } }
调用代码
extension MemberView { func save() -> Void { if (name.isEmpty) { nameError = "A name must be provided to continue" } if (username.isEmpty) { usernameError = "A username or email must be provided" } if (nameError != nil || usernameError != nil) { return } do { let mvm = MemberViewModel(name: name, username: username, password: password, age: age, role: role, admin: admin, imageName: AVATARS[selectedImage], imageTint: selectedTint) try mvm.create() page = nil } catch MemberViewModel.MemberError.UserExists(let username) { usernameError = "The username \(username) is already taken" } catch { } } }
问题原因
核心问题出在Core Data上下文的查询逻辑上:
- 在
init(name:...)构造方法中,你已经将新创建的Member实例添加到了Core Data的上下文里 - 调用
fetch方法时,查询的是同一个上下文,这个未保存但已存在于上下文的新实例会被查询到,导致fetch永远返回非nil,进而触发UserExists异常
修复方案
方案1:修改查询逻辑,排除当前未保存实例
在fetch方法中添加过滤条件,排除当前正在创建的未持久化实例:
static private func fetch(username: String, excludeNewEntity: Member? = nil) throws -> Member? { let fetchRequest: NSFetchRequest<Member> = Member.fetchRequest() let context = Self.container.viewContext // 基础用户名匹配条件 let usernamePredicate = NSPredicate(format: "username[c] == %@", username) var predicates = [usernamePredicate] // 排除当前正在创建的未保存实例 if let excludeEntity = excludeNewEntity { let excludePredicate = NSPredicate(format: "SELF != %@", excludeEntity) predicates.append(excludePredicate) } fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) return try context.fetch(fetchRequest).first }
同时修改create方法的调用逻辑,避免强制解包:
func create() throws { guard let username = entity?.username else { return } // 传入当前实例,排除查询结果中的自己 if try Self.fetch(username: username, excludeNewEntity: entity) != nil { throw MemberError.UserExists(username: username) } let context = Self.container.viewContext if context.hasChanges { try context.save() } }
方案2:先查询再创建实例(更推荐)
调整逻辑顺序,先检查数据库中是否存在重复用户名,确认不存在后再创建实例:
// 将构造逻辑改为静态创建方法 static func createMember(name: String, username: String, password: String, age: String?, role: String, admin: Bool, imageName: String, imageTint: String) throws -> MemberViewModel { let context = Self.container.viewContext // 先查询是否存在重复用户名 if try fetch(username: username) != nil { throw MemberError.UserExists(username: username) } // 确认无重复后再创建实例 let entity = Member(context: context) let vm = MemberViewModel(entity) vm.name = name vm.username = username vm.password = password vm.age = age ?? "" vm.role = role vm.admin = admin vm.imageName = imageName vm.imageTint = imageTint try context.save() return vm }
修改调用代码:
do { let mvm = try MemberViewModel.createMember(name: name, username: username, password: password, age: age, role: role, admin: admin, imageName: AVATARS[selectedImage], imageTint: selectedTint) page = nil } catch MemberViewModel.MemberError.UserExists(let username) { usernameError = "用户名 \(username) 已被占用" } catch { // 处理其他Core Data异常 }
额外优化建议
- 避免使用
entity!这类强制解包,改用guard let或if let处理可选值,防止崩溃 fetch方法中不要用try?吞掉错误,直接用try向上抛出,便于捕获Core Data查询异常
内容的提问来源于stack exchange,提问作者dohpaz42
相关产品推荐
相关产品推荐

