SwiftData — Relationship 的 deleteRule

在 Core Data 中,@RelationshipdeleteRule 指定了当删除源对象时,如何处理关联的目标对象。让我们来详细了解每种规则及其使用场景:

所有的 deleteRule 选项 #

public enum DeleteRule: Int16 {
    case noAction = 0        // 不执行任何操作
    case nullify = 1         // 将关系置为 nil
    case cascade = 2         // 级联删除关联对象
    case deny = 3           // 如果有关联对象则拒绝删除
}

1. noAction #

@Relationship(deleteRule: .noAction)
var items: [Item]
  • 行为:删除源对象时不对关联对象做任何处理
  • 使用场景
    • 当关联对象应该独立存在
    • 需要手动处理删除关系
  • 示例
// 部门和员工关系,删除部门不影响员工
class Department: NSManagedObject {
    @Relationship(deleteRule: .noAction)
    var employees: [Employee]
}

2. nullify #

@Relationship(deleteRule: .nullify)
var owner: User?
  • 行为:删除源对象时,将关联对象的反向关系置为 nil
  • 使用场景
    • 可选关系
    • 关联对象可以独立存在
  • 示例
// 用户和头像关系,删除用户时头像的 owner 置为 nil
class Avatar: NSManagedObject {
    @Relationship(deleteRule: .nullify)
    var owner: User?
}

3. cascade #

@Relationship(deleteRule: .cascade)
var messages: [Message]
  • 行为:删除源对象时级联删除所有关联对象
  • 使用场景
    • 强依赖关系
    • 父子关系
    • 所有权关系
  • 示例
// 聊天室和消息关系,删除聊天室时删除所有消息
class ChatRoom: NSManagedObject {
    @Relationship(deleteRule: .cascade)
    var messages: [Message]
}

4. deny #

@Relationship(deleteRule: .deny)
var items: [Item]
  • 行为:如果存在关联对象,则拒绝删除源对象
  • 使用场景
    • 需要强制保持数据完整性
    • 防止意外删除
  • 示例
// 类别和产品关系,有产品的类别不能删除
class Category: NSManagedObject {
    @Relationship(deleteRule: .deny)
    var products: [Product]
}

实际应用示例 #

1. 博客系统 #

// 博客文章和评论
class BlogPost: NSManagedObject {
    @Relationship(deleteRule: .cascade)  // 删除文章时删除所有评论
    var comments: [Comment]
    
    @Relationship(deleteRule: .nullify)  // 删除文章时作者关系置空
    var author: User?
    
    @Relationship(deleteRule: .deny)     // 有文章的标签不能删除
    var tags: [Tag]
}

2. 电子商务系统 #

// 订单系统
class Order: NSManagedObject {
    @Relationship(deleteRule: .cascade)  // 删除订单时删除所有订单项
    var orderItems: [OrderItem]
    
    @Relationship(deleteRule: .nullify)  // 删除订单时配送地址关系置空
    var shippingAddress: Address?
    
    @Relationship(deleteRule: .noAction) // 删除订单不影响用户
    var customer: User
}

3. 文件系统 #

// 文件夹结构
class Folder: NSManagedObject {
    @Relationship(deleteRule: .cascade)  // 删除文件夹时删除所有子文件夹
    var subfolders: [Folder]
    
    @Relationship(deleteRule: .cascade)  // 删除文件夹时删除所有文件
    var files: [File]
    
    @Relationship(deleteRule: .nullify)  // 删除文件夹时父文件夹关系置空
    var parent: Folder?
}

4. 社交网络 #

// 用户关系
class User: NSManagedObject {
    @Relationship(deleteRule: .cascade)  // 删除用户时删除所有发帖
    var posts: [Post]
    
    @Relationship(deleteRule: .nullify)  // 删除用户时好友关系置空
    var friends: [User]
    
    @Relationship(deleteRule: .cascade)  // 删除用户时删除个人资料
    var profile: Profile?
}

选择指南 #

  1. 使用 cascade 当:

    • 关联对象完全依赖于源对象
    • 是明确的所有权关系
    • 源对象是父对象
  2. 使用 nullify 当:

    • 关联是可选的
    • 对象可以独立存在
    • 需要保留关联对象
  3. 使用 deny 当:

    • 需要强制保持数据完整性
    • 防止重要数据被删除
    • 需要用户确认删除操作
  4. 使用 noAction 当:

    • 需要手动处理删除逻辑
    • 关联对象完全独立
    • 有特殊的删除要求

注意事项 #

  1. 数据一致性

    • 考虑关系的双向性
    • 确保删除规则不会造成数据不一致
  2. 性能影响

    • cascade 可能触发大量删除
    • deny 需要检查关联对象
  3. 用户体验

    • 考虑删除操作的可见性
    • 提供适当的错误处理
  4. 数据恢复

    • 考虑是否需要软删除
    • 备份重要数据
  5. 测试

    • 测试各种删除场景
    • 验证数据一致性

通过合理选择删除规则,可以构建更稳健的数据模型,确保应用数据的完整性和一致性。

多对多的关系应该怎么设置 #

比如 一条 todo 对应多条 tag,一条 tag 对应多条 todo。

在这种情况下,你应该使用 .nullify。让我解释原因和具体实现:

1. 数据模型设计 #

// Todo 实体
class Todo: NSManagedObject {
    @Relationship(deleteRule: .nullify) // ✅ 正确的选择
    var tags: [Tag]
}

// Tag 实体
class Tag: NSManagedObject {
    @Relationship(deleteRule: .nullify) // 反向关系也设置为 nullify
    var todos: [Todo]
}

2. 为什么选择 nullify #

  1. .nullify 的行为

    • 当删除 Todo 时,只会解除 Todo 和 Tag 之间的关联关系
    • Tag 实体本身不会被删除
    • 其他 Todo 与这些 Tag 的关联关系保持不变
  2. .noAction 的问题

    • 不会自动处理关系的解除
    • 可能导致悬空引用
    • 需要手动管理关系的清理

3. 实际使用示例 #

class TodoManager {
    let context: NSManagedObjectContext
    
    // 创建 Todo 和 Tag 的关联
    func createTodo(title: String, tags: [Tag]) {
        let todo = Todo(context: context)
        todo.title = title
        todo.tags = NSSet(array: tags)
        
        try? context.save()
    }
    
    // 删除 Todo
    func deleteTodo(_ todo: Todo) {
        // 使用 nullify 作为删除规则,以下关系处理会自动完成:
        // 1. Todo 被删除
        // 2. 相关的 Tag 对象保持不变
        // 3. Tag 和 Todo 之间的关联自动解除
        context.delete(todo)
        try? context.save()
    }
}

// 使用示例
class TodoListView: View {
    @Environment(\.managedObjectContext) private var context
    @State private var selectedTags: Set<Tag> = []
    
    var body: some View {
        List {
            ForEach(todos) { todo in
                TodoRow(todo: todo)
            }
            .onDelete { indexSet in
                // 删除 Todo 时,不用担心 Tag 的处理
                // Core Data 会自动处理关联关系
                for index in indexSet {
                    context.delete(todos[index])
                }
                try? context.save()
            }
        }
    }
}

4. 数据完整性验证 #

// 测试代码示例
func testTodoDeleteBehavior() {
    // 1. 创建测试数据
    let tag1 = Tag(context: context)
    tag1.name = "工作"
    
    let tag2 = Tag(context: context)
    tag2.name = "重要"
    
    let todo1 = Todo(context: context)
    todo1.title = "完成报告"
    todo1.tags = NSSet(array: [tag1, tag2])
    
    let todo2 = Todo(context: context)
    todo2.title = "开会"
    todo2.tags = NSSet(array: [tag1])
    
    try? context.save()
    
    // 2. 删除 todo1
    context.delete(todo1)
    try? context.save()
    
    // 3. 验证结果
    XCTAssertNotNil(tag1) // tag1 应该还存在
    XCTAssertNotNil(tag2) // tag2 应该还存在
    XCTAssertEqual(tag1.todos.count, 1) // tag1 还应该关联着 todo2
    XCTAssertEqual(tag2.todos.count, 0) // tag2 不再有关联的 todo
}

5. 最佳实践 #

  1. 双向关系的处理
// Tag 实体
class Tag: NSManagedObject {
    @Relationship(deleteRule: .nullify)
    var todos: [Todo]
    
    // 添加便利方法
    func addTodo(_ todo: Todo) {
        var currentTodos = todos ?? []
        currentTodos.append(todo)
        todos = currentTodos
    }
    
    func removeTodo(_ todo: Todo) {
        todos = todos?.filter { $0 != todo }
    }
}
  1. 批量操作的处理
func batchDeleteTodos(_ todos: [Todo]) {
    // 使用 nullify 作为删除规则,不需要特别处理 tag 关系
    todos.forEach { context.delete($0) }
    try? context.save()
}
  1. 关系变更的监控
extension Todo {
    override public func willChangeValue(forKey key: String) {
        super.willChangeValue(forKey: key)
        if key == "tags" {
            // 可以在这里添加关系变更的日志或其他处理
        }
    }
}

6. 总结 #

使用 .nullify 的优点:

  1. 自动处理关系的解除
  2. 保持数据完整性
  3. 不会意外删除 Tag
  4. 符合多对多关系的常见处理方式

这种设置确保了:

  • 删除 Todo 时不会影响现有的 Tag
  • 关系的解除是自动的
  • 数据模型保持清晰和一致
  • 避免了手动管理关系的复杂性

通过使用 .nullify,你可以安全地删除 Todo,而不用担心会影响到已有的 Tag,这正是你想要的行为。

本文共 1913 字,上次修改于 Jan 5, 2025