一、引言: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,不妨就从本文的例子开始,动手试一试吧 🙂