Section 8/181 menit

8. Schema Versioning & Migration

8. Schema Versioning & Migration

Struktur VersionedSchema

swift
import SwiftData

// Setiap versi schema didefinisikan dalam enum terpisah
// Model di dalam enum menggunakan nama yang sama tapi tipe berbeda

// Versi 1: User hanya punya name
enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] { [User.self, Task.self] }
    
    @Model final class User {
        var name: String
        var email: String
        
        init(name: String, email: String) {
            self.name = name
            self.email = email
        }
    }
    
    @Model final class Task {
        var title: String
        var isCompleted: Bool = false
        
        init(title: String) { self.title = title }
    }
}

// Versi 2: User punya firstName + lastName (split dari name)
//          Task punya dueDate yang baru
enum SchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] { [User.self, Task.self] }
    
    @Model final class User {
        var firstName: String
        var lastName: String
        var email: String
        
        init(firstName: String, lastName: String, email: String) {
            self.firstName = firstName
            self.lastName = lastName
            self.email = email
        }
        
        var fullName: String { "\(firstName) \(lastName)" }
    }
    
    @Model final class Task {
        var title: String
        var isCompleted: Bool = false
        var dueDate: Date?
        var priority: String = "medium"
        
        init(title: String) { self.title = title }
    }
}

// Versi 3: User punya profileImageData (external storage)
enum SchemaV3: VersionedSchema {
    static var versionIdentifier = Schema.Version(3, 0, 0)
    static var models: [any PersistentModel.Type] { [User.self, Task.self] }
    
    @Model final class User {
        var firstName: String
        var lastName: String
        var email: String
        
        @Attribute(.externalStorage)
        var profileImageData: Data?
        
        init(firstName: String, lastName: String, email: String) {
            self.firstName = firstName
            self.lastName = lastName
            self.email = email
        }
    }
    
    @Model final class Task {
        var title: String
        var isCompleted: Bool = false
        var dueDate: Date?
        var priority: String = "medium"
        
        init(title: String) { self.title = title }
    }
}

SchemaMigrationPlan

swift
// Definisikan migration path dari satu versi ke versi berikutnya
enum AppMigrationPlan: SchemaMigrationPlan {
    // Daftar semua versi schema yang pernah ada (urutan kronologis)
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self, SchemaV3.self]
    }
    
    // Stage migration yang perlu logika khusus
    static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3]
    }
    
    // V1 → V2: split "name" menjadi "firstName" dan "lastName"
    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self,
        willMigrate: nil,
        didMigrate: { context in
            // Setelah schema diupdate, isi nilai baru dari data lama
            // CATATAN: di sini kita akses SchemaV2.User (versi baru)
            let users = try context.fetch(FetchDescriptor<SchemaV2.User>())
            
            for user in users {
                // Data nama lama sudah di-migrate ke firstName oleh SwiftData
                // (karena nama kolom sama, SwiftData map otomatis)
                // Tapi lastName perlu diisi:
                if user.lastName.isEmpty {
                    // Split dari firstName jika memungkinkan
                    let parts = user.firstName.split(separator: " ", maxSplits: 1)
                    if parts.count == 2 {
                        user.firstName = String(parts[0])
                        user.lastName = String(parts[1])
                    } else {
                        user.lastName = ""  // Tidak ada last name
                    }
                }
            }
            
            try context.save()
        }
    )
    
    // V2 → V3: tambah profileImageData (nullable, default nil — tidak butuh logika khusus)
    static let migrateV2toV3 = MigrationStage.lightweight(
        fromVersion: SchemaV2.self,
        toVersion: SchemaV3.self
    )
}

// Gunakan di ModelContainer
let container = try ModelContainer(
    for: SchemaV3.User.self, SchemaV3.Task.self,
    migrationPlan: AppMigrationPlan.self
)

Lightweight vs Custom Migration

Perubahan Jenis Migration
Tambah property nullable baru Lightweight
Tambah property dengan default value Lightweight
Hapus property Lightweight
Rename property (dengan originalName) Lightweight
Rename entity (dengan originalName di @Model) Lightweight
Split/merge property Custom (butuh logic)
Ubah tipe property Custom (butuh logic)
Transformasi data non-trivial Custom (butuh logic)