WWDC Quick Look 💓 By SwiftGGTeam
Model your schema with SwiftData

Model your schema with SwiftData

观看原视频

Highlight

SwiftData 通过 @Model@Attribute@Relationship 等宏让数据建模完全代码化。开发者可以用 Swift 代码直接定义数据库 Schema、配置持久化行为、管理版本迁移,不需要任何外部文件格式。

核心内容

从 Core Data 到 SwiftData:建模方式的转变

以前用 Core Data 建模,需要打开 .xcdatamodeld 文件,在图形界面里添加 Entity、配置 Attribute、设置 Relationship。模型和代码分离,改一处要同步另一处,容易出错。

SwiftData 把建模完全搬进代码。给一个 Swift 类加上 @Model 宏,它就自动获得持久化能力。

00:56@Model 宏让 Trip 类自动遵循 PersistentModel 协议,框架根据属性定义生成完整的 Schema。代码即真理,模型定义就是 Schema 的单一来源。

@Attribute:精细控制属性行为

01:50)发布应用后发现同名 Trip 造成冲突。@Attribute(.unique) 可以确保属性值唯一,保存时执行 upsert 语义。想把 start_date 改成 startDate,但直接改名会被视为新属性导致数据丢失,@AttributeoriginalName 参数可以映射新旧属性名,避免数据丢失并实现平滑迁移。

详细内容

@Attribute:唯一约束与 upsert

01:50)发布应用后发现同名 Trip 造成冲突。用 @Attribute(.unique) 可以确保属性值唯一:

@Model
final class Trip {
    @Attribute(.unique) var name: String
    var destination: String
    var start_date: Date
    var end_date: Date
    
    var bucketList: [BucketListItem]? = []
    var livingAccommodation: LivingAccommodation?
}

关键点

  • @Attribute(.unique) 确保 name 字段值唯一
  • 保存时如果存在同名 Trip,执行 upsert(先尝试插入,冲突则更新)
  • 唯一约束适用于基础类型(数字、字符串、UUID)和一对一关系

@Attribute:属性重命名映射

02:48)想把 start_date 改成 startDate,但直接改名会被视为新属性,导致数据丢失。用 originalName 参数映射:

@Model
final class Trip {
    @Attribute(.unique) var name: String
    var destination: String
    @Attribute(originalName: "start_date") var startDate: Date
    @Attribute(originalName: "end_date") var endDate: Date
    
    var bucketList: [BucketListItem]? = []
    var livingAccommodation: LivingAccommodation?
}

关键点

  • originalName 把新属性名映射到数据库中的旧列名
  • 避免数据丢失,实现平滑迁移
  • @Attribute 还支持 .externalStorage(大数据存外部文件)和 transformable 类型

@Relationship:管理对象关系

级联删除

04:00)默认删除规则是 nullify——删除 Trip 时,关联的 bucketList 和 livingAccommodation 的外键被设为 nil,变成孤儿数据。想要删除 Trip 时一并删除关联数据:

@Model
final class Trip {
    @Attribute(.unique) var name: String
    var destination: String
    @Attribute(originalName: "start_date") var startDate: Date
    @Attribute(originalName: "end_date") var endDate: Date
    
    @Relationship(.cascade)
    var bucketList: [BucketListItem]? = []
  
    @Relationship(.cascade)
    var livingAccommodation: LivingAccommodation?
}

关键点

  • @Relationship(.cascade) 删除父对象时自动删除所有子对象
  • 隐式反向关系自动发现,不需要额外注解
  • @Relationship 还支持 originalName 和设置 to-many 关系的最小/最大数量

@Transient:排除不需要持久化的属性

04:54)想记录 Trip 被查看的次数,但不需要持久化:

@Model
final class Trip {
    @Attribute(.unique) var name: String
    var destination: String
    @Attribute(originalName: "start_date") var startDate: Date
    @Attribute(originalName: "end_date") var endDate: Date
    
    @Relationship(.cascade)
    var bucketList: [BucketListItem]? = []
  
    @Relationship(.cascade)
    var livingAccommodation: LivingAccommodation?

    @Transient
    var tripViews: Int = 0
}

关键点

  • @Transient 标记的属性不会存入数据库
  • 必须提供默认值,确保从 SwiftData 获取对象时有合理的初始值
  • 适合缓存、临时计算结果等不需要持久化的数据

Schema 迁移:版本演进的正确方式

07:12)应用发布多个版本后,Schema 需要演进。SwiftData 用 VersionedSchema 定义不同版本的 Schema:

enum SampleTripsSchemaV1: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [Trip.self, BucketListItem.self, LivingAccommodation.self]
    }

    @Model
    final class Trip {
        var name: String
        var destination: String
        var start_date: Date
        var end_date: Date
    
        var bucketList: [BucketListItem]? = []
        var livingAccommodation: LivingAccommodation?
    }
}

enum SampleTripsSchemaV2: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [Trip.self, BucketListItem.self, LivingAccommodation.self]
    }

    @Model
    final class Trip {
        @Attribute(.unique) var name: String
        var destination: String
        var start_date: Date
        var end_date: Date
    
        var bucketList: [BucketListItem]? = []
        var livingAccommodation: LivingAccommodation?
    }
}

enum SampleTripsSchemaV3: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [Trip.self, BucketListItem.self, LivingAccommodation.self]
    }

    @Model
    final class Trip {
        @Attribute(.unique) var name: String
        var destination: String
        @Attribute(originalName: "start_date") var startDate: Date
        @Attribute(originalName: "end_date") var endDate: Date
    
        var bucketList: [BucketListItem]? = []
        var livingAccommodation: LivingAccommodation?
    }
}

关键点

  • 每个版本是一个独立的 VersionedSchema 枚举
  • models 属性列出该版本包含的所有模型类型
  • 版本类内部定义该版本的 @Model

07:49)定义迁移计划:

enum SampleTripsMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SampleTripsSchemaV1.self, SampleTripsSchemaV2.self, SampleTripsSchemaV3.self]
    }
    
    static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3]
    }

    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: SampleTripsSchemaV1.self,
        toVersion: SampleTripsSchemaV2.self,
        willMigrate: { context in
            let trips = try? context.fetch(FetchDescriptor<SampleTripsSchemaV1.Trip>())
                      
            // De-duplicate Trip instances here...
                      
            try? context.save() 
        }, didMigrate: nil
    )
  
    static let migrateV2toV3 = MigrationStage.lightweight(
        fromVersion: SampleTripsSchemaV2.self,
        toVersion: SampleTripsSchemaV3.self
    )
}

关键点

  • schemas 列出所有版本,按顺序排列
  • stages 定义每两个版本之间的迁移步骤
  • .custom 迁移需要手动编写 willMigrate 闭包处理数据转换
  • .lightweight 迁移由框架自动处理(如属性重命名)
  • willMigrate 在 Schema 变更前执行,可以查询旧版本数据做预处理
  • didMigrate 在 Schema 变更后执行,可以验证迁移结果

08:40)在应用启动时配置迁移计划:

struct TripsApp: App {
    let container = ModelContainer(
        for: Trip.self, 
        migrationPlan: SampleTripsMigrationPlan.self
    )
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(container)
    }
}

关键点

  • ModelContainer 初始化时传入 migrationPlan
  • 框架自动检测当前数据库版本,按顺序执行需要的迁移
  • 迁移在后台线程执行,完成后应用正常启动

核心启发

用唯一约束简化数据同步

  • 做什么:为具有业务唯一性的字段(如用户名、订单号)添加 @Attribute(.unique)
  • 为什么值得做:upsert 语义让同步逻辑更简单,不需要先查询再决定插入还是更新
  • 怎么开始:在 @Model 类中为关键字段添加 @Attribute(.unique)

为大数据属性启用外部存储

  • 做什么:图片、音频等二进制数据使用 @Attribute(.externalStorage)
  • 为什么值得做:减少数据库文件体积,提升查询性能,大数据存取更高效
  • 怎么开始:将图片等属性标记为 @Attribute(.externalStorage),SwiftData 自动管理文件存储

设计合理的删除级联策略

  • 做什么:明确每个父子关系的删除行为,用 @Relationship(.cascade).nullify
  • 为什么值得做:避免孤儿数据,保持数据完整性
  • 怎么开始:分析数据模型中的关系,为需要级联删除的关系添加 @Relationship(.cascade)

尽早建立版本化 Schema 和迁移计划

  • 做什么:从第一个版本开始就使用 VersionedSchema,即使当前不需要迁移
  • 为什么值得做:后期补迁移计划比一开始就规划要困难得多
  • 怎么开始:定义 SchemaV1,即使内容很简单。后续版本递增编号,每次变更都更新迁移计划

关联 Session

评论

GitHub Issues · utterances