לעתים קרובות אנו נתקלים במצבים שבהם loadPersistentStores מחזיר NSMigrationError—והאפליקציה קורסת בעת ההפעלה. ב-80% מהמקרים, הסיבה היא גרסת מודל שגויה: מפתח הוסיף תכונה חדשה ב-.xcdatamodeld, שכח ליצור גרסה חדשה, והאפליקציה מזהה אי-התאמה בין הקוד למאגר. עבור המשתמש, מדובר בקריסה בעת ההפעלה. עבור הצוות, זה תיקון חירום בשעה 2 בלילה. במשך 5 שנים, השלמנו בהצלחה מעל 50 הגירות Core Data עבור לקוחות מתוך 100 האפליקציות המובילות ב-App Store, וחסכנו להם בממוצע 30% מזמן התחזוקה.
אילו בעיות פותרת הגירת Core Data?
הגירת Core Data פותרת שתי משימות עיקריות: שימור נתונים קיימים בעת עדכון הסכמה והבטחת תאימות בין גרסאות האפליקציה. בלעדיה, כל שינוי במודל מוביל לאובדן נתונים או לקריסה. הגירה קלה (Lightweight) מטפלת אוטומטית בשינויים פשוטים (הוספת תכונות אופציונליות, הסרת שדות), בעוד שהגירה כבדה (Heavyweight) מטפלת בטרנספורמציות מורכבות. לדוגמה, שינוי סוג שדה מ-Float ל-Int64 דורש מדיניות מותאמת אישית.
הגירה קלה: מתי היא עובדת וכיצד להגדיר אותה
הגירה קלה (NSInferMappingModelAutomatically) פועלת אוטומטית עבור הוספת תכונה חדשה עם optional = true או ערך ברירת מחדל, הסרת תכונה, או שינוי שם באמצעות Renaming Identifier. היא מופעלת עם שורת קוד אחת:
let options: [String: Any] = [
NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true
]
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)עבור let options: [String: Any] = [ NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true ] try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options) :
container.persistentStoreDescriptions.first?.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions.first?.shouldInferMappingModelAutomatically = trueחשוב: לעולם אל תערוך גרסת מודל קיימת אם האפליקציה כבר בפרודקשן. במקום זאת, צור גרסה חדשה דרך Editor → Add Model Version והגדר אותה כגרסה הנוכחית (Current Version).
הגירה כבדה: כאשר האוטומציה אינה מספיקה
אם סוג תכונה השתנה, שדה חובה לא-אופציונלי נוסף ללא ברירת מחדל, או שנדרשת טרנספורמציה של נתונים—נדרשת NSPersistentContainer מותאמת אישית. דוגמה להמרת מחרוזת למספר:
class TransactionMigrationPolicy: NSEntityMigrationPolicy {
override func createDestinationInstances(
forSource sourceInstance: NSManagedObject,
in mapping: NSEntityMapping,
manager: NSMigrationManager
) throws {
let destination = NSEntityDescription.insertNewObject(
forEntityName: mapping.destinationEntityName!,
into: manager.destinationContext
)
destination.setValue(sourceInstance.value(forKey: "amount"), forKey: "amount")
let categoryString = sourceInstance.value(forKey: "category") as? String ?? ""
destination.setValue(CategoryMapper.intValue(for: categoryString), forKey: "categoryRaw")
manager.associate(sourceInstance: sourceInstance, withDestinationInstance: destination, for: mapping)
}
}ה-Mapping Model נוצר ב-Xcode: New File → Mapping Model. שם מציינים אילו ישויות משתמשות במדיניות המותאמת אישית.
השוואה: הגירה קלה לעומת כבדה
| פרמטר | קלה | כבדה |
|---|---|---|
| מהירות | מיידית עבור נתונים קטנים | עד מספר שניות במאגרים גדולים |
| אוטומציה | 100% | דורשת קוד Policy |
| מורכבות השינוי | פשוטה (הוספה/הסרה/שינוי שם) | כל שינוי (טרנספורמציה, מיזוג, פיצול) |
| סיכונים | מינימליים | גבוהים (נדרש גיבוי) |
| כמות קוד | שורת אפשרויות אחת | 50–200 שורות |
| פיתוח | מהיר פי 10 | ארוך יותר |
הגירה קלה מהירה פי 10 מהגירה כבדה ויציבה יותר עבור שינויים פשוטים. אם אינך בטוח לגבי האסטרטגיה, בקש ייעוץ—נעזור לך לבחור בגישה האופטימלית.
כיצד פועלת הגירה פרוגרסיבית
אם משתמש לא עדכן מ-v1 ל-v5, Core Data לא משרשרת הגירות אוטומטית. נדרש מנהל (manager) כדי להחיל ברצף את כל הגרסאות מהנוכחית ליעד:
class MigrationManager {
func migrateStore(at storeURL: URL) throws {
var currentURL = storeURL
while true {
guard let sourceModel = NSManagedObjectModel.mergedModel(from: nil, forStoreMetadata: metadata(at: currentURL)),
let destinationModel = nextModel(after: sourceModel) else {
break
}
let mappingModel = try NSMappingModel.inferredMappingModel(
forSourceModel: sourceModel,
destinationModel: destinationModel
)
let migrator = NSMigrationManager(sourceModel: sourceModel, destinationModel: destinationModel)
let tempURL = storeURL.appendingPathExtension("migration")
try migrator.migrateStore(from: currentURL, type: .sqlite, to: tempURL, type: .sqlite, mapping: mappingModel)
try FileManager.default.removeItem(at: currentURL)
try FileManager.default.moveItem(at: tempURL, to: storeURL)
}
}
}ההגירה מתבצעת לפני אתחול container.persistentStoreDescriptions.first?.shouldMigrateStoreAutomatically = true container.persistentStoreDescriptions.first?.shouldInferMappingModelAutomatically = true —במסך הפתיחה (splash screen) עם מחוון התקדמות. הגירה פרוגרסיבית מפחיתה את זמן ההשבתה ב-70% בהשוואה ליצירה מחדש מלאה של מסד הנתונים.
מדוע גיבוי הוא קריטי
בצע תמיד גיבוי לפני הגירה כבדה:
let backupURL = storeURL.deletingLastPathComponent()
.appendingPathComponent("backup_\(Date().timeIntervalSince1970).sqlite")
try FileManager.default.copyItem(at: storeURL, to: backupURL)אם ההגירה נכשלת, שחזר את הגיבוי. זה קריטי עבור נתונים שלא ניתן לשחזר.
כיצד לבצע הגירה: הוראות שלב-אחר-שלב
- בצע ביקורת על המודל הנוכחי: בדוק את היסטוריית הגרסאות וסוגי השינויים.
- צור גרסת מודל חדשה ב-Xcode (Editor → Add Model Version).
- הגדר אותה כגרסה הנוכחית (Current Version).
- הגדר הגירה קלה (הוסף דגלים) או צור Mapping Model עבור הגירה כבדה.
- כתוב
NSEntityMigrationPolicyמותאם אישית אם נדרשת טרנספורמציה של נתונים. - יישם הגירה פרוגרסיבית (אם המשתמש דילג על גרסאות).
- בצע גיבוי של קובץ ה-SQLite.
- הרץ את ההגירה לפני אתחול הקונטיינר במסך הפתיחה.
- בדוק עם
class TransactionMigrationPolicy: NSEntityMigrationPolicy { override func createDestinationInstances( forSource sourceInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager ) throws { let destination = NSEntityDescription.insertNewObject( forEntityName: mapping.destinationEntityName!, into: manager.destinationContext ) destination.setValue(sourceInstance.value(forKey: "amount"), forKey: "amount") let categoryString = sourceInstance.value(forKey: "category") as? String ?? "" destination.setValue(CategoryMapper.intValue(for: categoryString), forKey: "categoryRaw") manager.associate(sourceInstance: sourceInstance, withDestinationInstance: destination, for: mapping) } }אמיתי בסימולטור ובמכשיר.
שגיאות נפוצות
| שגיאה | סיבה | פתרון |
|---|---|---|
class MigrationManager { func migrateStore(at storeURL: URL) throws { var currentURL = storeURL while true { guard let sourceModel = NSManagedObjectModel.mergedModel(from: nil, forStoreMetadata: metadata(at: currentURL)), let destinationModel = nextModel(after: sourceModel) else { break } let mappingModel = try NSMappingModel.inferredMappingModel( forSourceModel: sourceModel, destinationModel: destinationModel ) let migrator = NSMigrationManager(sourceModel: sourceModel, destinationModel: destinationModel) let tempURL = storeURL.appendingPathExtension("migration") try migrator.migrateStore(from: currentURL, type: .sqlite, to: tempURL, type: .sqlite, mapping: mappingModel) try FileManager.default.removeItem(at: currentURL) try FileManager.default.moveItem(at: tempURL, to: storeURL) } } } |
אי-התאמה בסכמה | צור גרסת מודל חדשה |
| מסך פתיחה תקוע | הגירה כבדה על ה-thread הראשי | בצע ברקע עם מחוון התקדמות |
| אובדן נתונים | אין גיבוי | תמיד העתק את המאגר לפני הגירה |
NSPersistentContainer |
עריכת הגרסה הנוכחית | השתמש ב-Add Model Version |
מה כלול וזמני ביצוע
- ביקורת על המודל הנוכחי והיסטוריית הגרסאות
- יצירת גרסאות
let backupURL = storeURL.deletingLastPathComponent() .appendingPathComponent("backup_\(Date().timeIntervalSince1970).sqlite") try FileManager.default.copyItem(at: storeURL, to: backupURL)חדשות - הגירה קלה או כבדה בהתאם לשינויים
NSEntityMigrationPolicyמותאם אישית לטרנספורמציה של נתונים- הגירה פרוגרסיבית על פני מספר גרסאות
- גיבוי לפני הגירה
הגירה קלה (הוספת תכונות) אורכת 0.5 ימים (החל מ-$500). הגירה כבדה עם מדיניות מותאמת אישית ומעברים פרוגרסיביים אורכת 2–3 ימים (החל מ-$1500). צור קשר לביקורת על המודל שלך—נבחר את אסטרטגיית ההגירה האופטימלית. בקש ייעוץ וקבל תוכנית עבודה מפורטת.
קרא עוד על הגירת Core Data בתיעוד הרשמי של Apple.







