You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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上下文的查询逻辑上:

  1. 在init(name:...)构造方法中,你已经将新创建的Member实例添加到了Core Data的上下文里
  2. 调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 03:15:59