一、引言:Core Data,在本地數據持久化中的地位
在 iOS 開發中,本地數據存儲幾乎是每一個 App 都繞不開的問題。無論是緩存用戶信息、離線瀏覽內容,還是記錄用戶操作歷史,一個合適的數據持久化方案都能大大提升應用的體驗和性能。
而說到蘋果官方提供的解決方案,Core Data?無疑是最具代表性的工具之一。它不僅僅是一個數據庫封裝工具,更是一個面向對象的數據模型框架,允許我們以結構化方式描述實體之間的關系,并通過上下文(Context)來完成對象的創建、查詢、更新和刪除。
很多人可能對 Core Data 保持一定的距離,覺得它“上手復雜”“冗余配置多”,但其實,Core Data 在近年來的演進中已經發生了非常大的變化:
- NSPersistentContainer?的引入,極大簡化了配置步驟;
- Xcode 支持自動生成模型類,無需手動維護 NSManagedObject 子類;
- 與 SwiftUI 的集成也越來越順暢(雖然本文暫不涉及);
在這篇文章中,我們將以兩個簡單的實體模型為例:PHDramaEntity?和?PHEpisodeEntity,構建一個“一對多”的關系結構,完整演示 Core Data 從模型創建到數據操作的基本用法。
無論你是第一次接觸 Core Data,還是想重新認識這個框架,相信這篇文章都能帶你快速上手,并掌握它的核心用法。
二、創建 Core Data 模型文件與實體類
在使用 Core Data 之前,我們第一步需要做的,就是創建一個數據模型文件,并在其中定義好我們項目所需的實體(Entity)、屬性(Attribute)和關系(Relationship)。
🛠? 新建?.xcdatamodeld?文件
首先,我們在項目中新增一個 Core Data 模型文件:文件名:AmericanDramaDB。你可以起任何名字,也可以直接使用默認的名稱。
你可以通過 Xcode 的「File → New → File… → Data Model」選項來添加這個模型文件。
📦 創建?PHDramaEntity?實體
接下來,我們在模型中創建一個名為?PHDramaEntity?的實體,它表示一部劇,包含劇名、封面、分組信息等字段。同時,它還會關聯多個劇集。
實體字段設計如下:
- id: Int64
- title: String?
- coverFileName: String?
- packageID: String?
- type: Int64
- index: Int64
- episodeList: To-Many 關系(指向 PHEpisodeEntity)
其中?episodeList?是一對多關系,表示該劇所擁有的所有劇集。Core Data 支持我們在模型中直接配置這種實體間的引用關系。
Xcode 會根據模型自動生成如下代碼:
extension PHDramaEntity {@nonobjc public class func fetchRequest() -> NSFetchRequest<PHDramaEntity> {return NSFetchRequest<PHDramaEntity>(entityName: "PHDramaEntity")}@NSManaged public var coverFileName: String?@NSManaged public var id: Int64@NSManaged public var index: Int64@NSManaged public var packageID: String?@NSManaged public var title: String?@NSManaged public var type: Int64@NSManaged public var episodeList: NSSet?
}// MARK: - Generated accessors for episodeList
extension PHDramaEntity {@objc(addEpisodeListObject:)@NSManaged public func addToEpisodeList(_ value: PHEpisodeEntity)@objc(removeEpisodeListObject:)@NSManaged public func removeFromEpisodeList(_ value: PHEpisodeEntity)@objc(addEpisodeList:)@NSManaged public func addToEpisodeList(_ values: NSSet)@objc(removeEpisodeList:)@NSManaged public func removeFromEpisodeList(_ values: NSSet)
}extension PHDramaEntity : Identifiable { }
你到不需要找到代碼在哪,編譯成功之后就可以直接使用這個實體及其相關的屬性和方法。
🎞? 創建?PHEpisodeEntity?實體
接下來是?PHEpisodeEntity,它代表具體的某一集劇集,字段更豐富一些,還關聯了卡片、閱讀記錄等內容。
字段設計如下:
- id: Int64
- title: String?
- index: Int64(集數順序)
- episodeCoverFileName: String?
- packageId: String?
- dramaId: Int64
- progress: Double
- readDate: Date?
- type: Int64
- cardList: To-Many(指向 PHCardEntity)
- readDateList: To-Many(指向 PHReadRecordEntity)
- drama: To-One(指向 PHDramaEntity,作為反向引用)
我們可以只考慮PHEpisodeEntity和PHDramaEntity兩個實體,其它實體和關系暫且不需要考慮,不影響對Core Data使用的理解。
Xcode 同樣生成如下代碼:
extension PHEpisodeEntity {@nonobjc public class func fetchRequest() -> NSFetchRequest<PHEpisodeEntity> {return NSFetchRequest<PHEpisodeEntity>(entityName: "PHEpisodeEntity")}@NSManaged public var dramaId: Int64@NSManaged public var episodeCoverFileName: String?@NSManaged public var id: Int64@NSManaged public var index: Int64@NSManaged public var packageId: String?@NSManaged public var progress: Double@NSManaged public var readDate: Date?@NSManaged public var title: String?@NSManaged public var type: Int64@NSManaged public var cardList: NSSet?@NSManaged public var drama: PHDramaEntity?@NSManaged public var readDateList: NSSet?
}// MARK: - Generated accessors for cardList
extension PHEpisodeEntity {@objc(addCardListObject:)@NSManaged public func addToCardList(_ value: PHCardEntity)@objc(removeCardListObject:)@NSManaged public func removeFromCardList(_ value: PHCardEntity)@objc(addCardList:)@NSManaged public func addToCardList(_ values: NSSet)@objc(removeCardList:)@NSManaged public func removeFromCardList(_ values: NSSet)
}// MARK: - Generated accessors for readDateList
extension PHEpisodeEntity {@objc(addReadDateListObject:)@NSManaged public func addToReadDateList(_ value: PHReadRecordEntity)@objc(removeReadDateListObject:)@NSManaged public func removeFromReadDateList(_ value: PHReadRecordEntity)@objc(addReadDateList:)@NSManaged public func addToReadDateList(_ values: NSSet)@objc(removeReadDateList:)@NSManaged public func removeFromReadDateList(_ values: NSSet)
}extension PHEpisodeEntity : Identifiable { }
值得一提的是,drama?是一個?反向關系,它使我們可以在訪問劇集時,直接找到它所屬的劇,方便非常多。
以上就是使用 Core Data 創建數據模型的全過程。
下一步,我們將配置 Core Data 棧,準備好?NSPersistentContainer?和上下文,真正開始使用 Core Data 的增刪改查功能。
三、配置 Core Data 棧:管理上下文與持久容器
完成模型文件的創建后,我們就可以正式初始化 Core Data 的持久化棧了。這一步的核心就是構建?NSPersistentContainer,并拿到?NSManagedObjectContext,用于后續的數據操作。
為此,我們可以創建一個專門的 Core Data 管理類,比如命名為?PHCoreDataManager,采用單例模式進行統一管理。
🧱 創建 Core Data 管理類
import CoreDataclass PHCoreDataManager: NSObject {static let shared = PHCoreDataManager()let container: NSPersistentContainervar context: NSManagedObjectContext {container.viewContext}private override init() {container = NSPersistentContainer(name: "AmericanDramaDB")container.loadPersistentStores { description, error inif let error = error {fatalError("Core Data 加載失敗: \(error)")}}}/// 保存上下文func saveContext() {do {try context.save()} catch {print("保存失敗: \(error)")}}
}
📌 關鍵解釋
- NSPersistentContainer(name:)?中的參數要與你的?.xcdatamodeld?文件名一致(不含擴展名),否則會找不到模型。
- loadPersistentStores?是異步加載持久化存儲的過程,建議在其中加上錯誤處理。
- container.viewContext?是我們最常使用的上下文,用于主線程讀寫操作。
- saveContext()?方法建議封裝在這里,方便統一調用,避免遺漏保存。
??注意:Core Data 的操作都基于 context,創建、修改、刪除對象后都需要調用?save()?才會真正落盤。如果不保存,應用重啟后數據會丟失。
四、增刪改查:以 PHDramaEntity 為例
有了模型和 Core Data 棧之后,我們終于可以開始使用 Core Data 進行數據操作了。下面我們就以?PHDramaEntity為例,演示最常見的增、刪、改、查操作。
1?? 插入數據(Create)
我們先演示如何創建一條新的?PHDramaEntity?數據,并保存到數據庫中。
let context = PHCoreDataManager.shared.contextlet drama = PHDramaEntity(context: context)
drama.id = 1001
drama.title = "絕命毒師"
drama.coverFileName = "breaking_bad.jpg"
drama.packageID = "breaking-bad"
drama.type = 1
drama.index = 0PHCoreDataManager.shared.saveContext()
每次創建實體對象時,都需要傳入?context,這是 Core Data 的核心機制。
2?? 查詢數據(Read)
通過?NSFetchRequest?可以查詢所有?PHDramaEntity?數據,按標題排序:
let request: NSFetchRequest<PHDramaEntity> = PHDramaEntity.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "title", ascending: true)]do {let dramas = try context.fetch(request)for drama in dramas {print("劇名:\(drama.title ?? "未知"),封面:\(drama.coverFileName ?? "無")")}
} catch {print("查詢失敗:\(error)")
}
你可以通過?NSPredicate?添加條件過濾,比如查找指定 packageID 的劇。
3?? 更新數據(Update)
我們以“更新某個指定 ID 的劇的標題”為例:
let request: NSFetchRequest<PHDramaEntity> = PHDramaEntity.fetchRequest()
request.predicate = NSPredicate(format: "id == %d", 1001)do {if let drama = try context.fetch(request).first {drama.title = "絕命毒師(更新后)"PHCoreDataManager.shared.saveContext()print("更新成功")}
} catch {print("更新失敗:\(error)")
}
只需要修改對象屬性后再?saveContext()?即可完成更新。
4?? 刪除數據(Delete)
刪除也是非常直接,只需要調用?context.delete(_:):
let request: NSFetchRequest<PHDramaEntity> = PHDramaEntity.fetchRequest()
request.predicate = NSPredicate(format: "id == %d", 1001)do {if let drama = try context.fetch(request).first {context.delete(drama)PHCoreDataManager.shared.saveContext()print("刪除成功")}
} catch {print("刪除失敗:\(error)")
}
刪除對象后,也一定記得調用?saveContext(),否則不會真正從數據庫移除。
五、操作一對多關系:從劇到劇集的增查操作
在前面我們已經定義好了?PHDramaEntity?和?PHEpisodeEntity?的一對多關系:一個劇(Drama)包含多個劇集(Episode),我們通過?episodeList?來描述這種引用。
本節將演示兩個核心操作:
- 如何向?PHDramaEntity?添加多個?PHEpisodeEntity
- 如何從一個劇中讀取它的所有劇集
1?? 添加多個劇集到某個劇
假設我們已經有一個?PHDramaEntity?對象,接下來我們要為它添加兩集內容。
let context = PHCoreDataManager.shared.context// 創建劇
let drama = PHDramaEntity(context: context)
drama.id = 2001
drama.title = "紙牌屋"
drama.packageID = "house-of-cards"
drama.index = 1
drama.type = 1// 創建劇集 1
let ep1 = PHEpisodeEntity(context: context)
ep1.id = 1
ep1.title = "第一集"
ep1.index = 1
ep1.drama = drama // 反向關聯// 創建劇集 2
let ep2 = PHEpisodeEntity(context: context)
ep2.id = 2
ep2.title = "第二集"
ep2.index = 2
ep2.drama = drama // 同樣反向關聯// 保存
PHCoreDataManager.shared.saveContext()
??推薦做法:通過設置劇集的 drama 屬性來建立反向引用關系,Core Data 會自動同步 episodeList。
當然你也可以反向添加:
drama.addToEpisodeList(ep1)
drama.addToEpisodeList(ep2)
兩種方式等效,哪種更符合你的使用習慣都可以。
2?? 從劇中讀取所有劇集
Core Data 一對多關系的字段類型通常是?NSSet?,所以我們需要進行類型轉換。
if let episodeSet = drama.episodeList as? Set<PHEpisodeEntity> {let sortedEpisodes = episodeSet.sorted { $0.index < $1.index }for episode in sortedEpisodes {print("劇集 \(episode.index):\(episode.title ?? "未知")")}
}
為了更方便使用,你也可以在?PHDramaEntity?中擴展一個 computed property:
extension PHDramaEntity {var sortedEpisodeArray: [PHEpisodeEntity] {let set = episodeList as? Set<PHEpisodeEntity> ?? []return set.sorted { $0.index < $1.index }}
}
這樣你就可以直接這樣用:
for episode in drama.sortedEpisodeArray {print(episode.title ?? "")
}
六、結語:Core Data,其實沒你想的那么復雜
在這篇文章中,我們從零開始,一步步搭建了 Core Data 的使用框架:
- 創建數據模型文件,并定義實體和一對多關系;
- 初始化 Core Data 棧,封裝?NSPersistentContainer;
- 演示了如何對實體進行增刪改查操作;
- 展示了一對多關系的建立與遍歷方式;
你可以看到,Core Data 的使用并沒有傳說中那么復雜。隨著?NSPersistentContainer?的出現,以及 Xcode 對模型類的自動生成支持,開發者已經可以非常高效地在項目中集成本地數據持久化功能。
當然,本文只是 Core Data 的起點。后續你還可以探索:
- 如何設置刪除規則(Cascade、Nullify 等);
- 如何使用?NSFetchedResultsController?優雅地驅動 UI;
- 如何與 SwiftUI 結合,使用?@FetchRequest?實時監聽數據變化;
- 如何進行數據遷移(Model Versioning);
但只要你掌握了本文的內容,Core Data 的世界就已經向你敞開大門。
如果你還沒在項目中使用過 Core Data,不妨就從本文的例子開始,動手試一試吧 🙂