了解和采用 CoreData 框架 —— Ficow 陪你学 CoreData

| iOS , Xcode , CoreData , 数据库

 

内容概览

  • 前言
  • CoreData 核心概念
  • 创建和加载 CoreData 数据模型
  • 检查 CoreData 数据库文件
  • 总结

 

前言

 

CoreData 是苹果官方提供的数据存储框架,其底层实现对 SQLite 进行了封装,提供了对象-关系映射(ORM)功能。

CoreData 具有以下优势:

  • 提供面向对象风格的接口来操作数据,不再需要开发人员手动编写 SQL 语句
  • 提供了高效、易用的版本迁移支持;
  • 苹果官方开发和维护的框架,质量、性能和安全性以及后期更新维护方面都有绝对的保障;
  • 支持易用的撤销、重做功能以及视图同步功能;

同时,CoreData 还具有以下劣势:

  • 学习成本略高;
  • 易用性不如 Realm 等数据库;
  • 性能不如 WCDB 等数据库;
  • 如果需要直接写 SQL 语句,FMDB 会更易用;

Ficow 曾经在参与过的项目中使用过 CoreData, Realm 以及 FMDB。个人的主观感受是 Realm 最易用,FMDB 易用性差(手写SQL),CoreData 概念较多。尽管如此,对于 iOS 开发人员来说,CoreData 依然是一个值得学习的框架。

请注意,本文基于 Xcode 12 进行创作。

 

CoreData 核心概念

 

CoreData 中涉及到的核心概念:

  • NSPersistentContainer,用于配置模型、上下文和存储协调器;
  • NSPersistentStoreCoordinator,与数据库交互,同步应用中的数据库改动到底层的数据库;
  • NSManagedObjectContext,在内存中记录数据库类型实例的改动;
  • NSManagedObjectModel,描述应用中的数据库模型类型、属性和关系的模型文件(数据库表);

这些类型是 Core Data 栈的核心,如果您感兴趣可以查看 完整的 Core Data 栈

在 NSPersistentContainer 实例被创建后,它的 managedObjectModel, viewContextpersistentStoreCoordinator 属性会分别指向自动创建的模型、上下文和存储协调器实例。

 

创建和加载 CoreData 数据模型

 

什么是 CoreData 数据模型?

  • CoreData 数据模型用于描述应用中的数据库实体、属性以及实体间的关系。
  • CoreData 数据模型文件的扩展名为:.xcdatamodeld
  • 数据库模型越丰富,CoreData 数据模型就更能突显自身的优势。

比如,下方的官方文档示例图中的 Employee 实体(Entity),其具有多个属性(Attribute),还具有和 Department 实体之间的关系(Relationship)。

 

创建 CoreData 数据模型的常见方式有两种。

 

第一种,在创建项目时勾选 Use Core Data 选项

Xcode 会自动为您创建一个与项目同名的 .xcdatamodeld 数据模型文件:

并且在 AppDelegateSceneDelegate 中自动添加 Core Data 相关代码(为方便演示,函数体已被 Ficow 折叠):

请注意,AppDelegate 中自动生成的 Core Data 相关代码尚未正确地处理错误情况!即使发生了可以预见的错误(如:设备存储空间不足、数据模型迁移失败等),程序也会调用 fatalError,然后崩溃!不过,这是一个具有争议的话题

 

第二种,在项目中手动添加

您需要手动添加一个 .xcdatamodeld 数据模型文件:

找到 Core Data 数据模型文件,然后添加:

然后,您可以在项目中找到这个数据模型文件:

此时,Xcode 中没有操作这个数据模型文件的代码,所以您还需要添加相应的代码。

此处附上 Xcode 在 AppDelegate 中自动生成的代码:

    // MARK: - Core Data stack

    lazy var persistentContainer: NSPersistentContainer = {
        /*
         The persistent container for the application. This implementation
         creates and returns a container, having loaded the store for the
         application to it. This property is optional since there are legitimate
         error conditions that could cause the creation of the store to fail.
        */

        // !!!!!!!!!!请注意检查文件名!!!!!!!!!!
        let container = NSPersistentContainer(name: "Model")
        // !!!!!!!!!!请注意检查文件名!!!!!!!!!!

        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

                /*
                 Typical reasons for an error here include:
                 * The parent directory does not exist, cannot be created, or disallows writing.
                 * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                 * The device is out of space.
                 * The store could not be migrated to the current model version.
                 Check the error message to determine what the actual problem was.
                 */
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

    // MARK: - Core Data Saving support

    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }

要注意检查 let container = NSPersistentContainer(name: "Model") 中的文件名是否正确!

然后,这是在 SceneDelegate 中自动生成的代码:

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.

        // Save changes in the application's managed object context when the application transitions to the background.
        (UIApplication.shared.delegate as? AppDelegate)?.saveContext()
    }

但是,Ficow 将这些代码搬到了一个单例 DBHelper 中。这样封装之后,可以方便地管理数据库操作相关的代码,也方便以后很容易地使用其他数据库替换 CoreData

final class DBHelper {

    static let shared = DBHelper()

    var viewContext: NSManagedObjectContext {
        return persistentContainer.viewContext
    }

    var newBackgroundContext: NSManagedObjectContext {
        let context = persistentContainer.newBackgroundContext()
        context.automaticallyMergesChangesFromParent = true
        context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
        return context
    }

    private(set) lazy var persistentContainer: NSPersistentContainer = {
        //  !!!!!!!!!!请注意检查文件名!!!!!!!!!!
        let container = NSPersistentContainer(name: "Model")
        //  !!!!!!!!!!请注意检查文件名!!!!!!!!!!
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                #if DEBUG
                    assertionFailure("Run \(#function) failed, error \(error), \(error.userInfo)")
                #endif
            }
        })
        // https://stackoverflow.com/questions/55678116/automaticallymergeschangesfromparent-doesnt-do-anything
        container.viewContext.automaticallyMergesChangesFromParent = true
        // https://stackoverflow.com/questions/8134562/which-nsmergepolicy-do-i-use
        container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
        return container
    }()

    private init () {}

    // MARK: - Core Data Saving support

    func saveChanges() {
        let context = persistentContainer.viewContext
        try? context.saveIfNeeded()
    }
}

除此之外,还封装了一个 NSManagedObjectContext 类型的扩展,便于之后保存 context 内的改动:

extension NSManagedObjectContext {
    func saveIfNeeded(resetContext: Bool = false) throws {
        if self.hasChanges {
            do {
                try self.save()
            } catch let error as NSError  {
                #if DEBUG
                    assertionFailure("Run \(#function) failed, error: \(error.userInfo)")
                #endif
                throw error
            }
            if resetContext { self.reset() }
        }
    }
}

最后,只需要在 AppDelegate 中调用存储方法即可:

    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
        // Saves changes in the application's managed object context before the application terminates.
        DBHelper.shared.saveChanges()
    }

 

检查 CoreData 数据库文件

 

默认情况下,CoreData 使用的 SQLite 数据库文件存储在应用的库路径中。您可以在应用中输出数据库所在的位置:

let defaultLibraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
print(defaultLibraryDirectory)

输出路径:

/Users/mac_user_name/Library/Developer/CoreSimulator/Devices/7867070C-E3A4-4662-AE02-50F3333E3318/data/Containers/Data/Application/EFB37279-8C58-4FDC-B0DB-44974CBAC470/Library

在这个路径下,打开 Application Support 文件夹,您可以看到相应的 .sqlite 文件。

然后,您可以选择使用某些数据库浏览器打开这个文件查看其中的内容,比如:DB Browser for SQLite

DB Browser for SQLite 的用户界面如下图所示:

您可以很方便地查看表、索引、数据,还可以执行 SQL 来操作数据库。

 

总结

 

在集成 CoreData 的过程中,一定要注意封装相关的API,以便以后迁移底层的数据库框架。

需要注意的是,一定只在数据库封装层使用数据库中的模型,相关的API定义需要脱离数据库模型类型。作为参考,您可以考虑使用 struct 或者 protocol 等类型来定义相关的API涉及到的数据库模型类型。
这样做,一方面可以方便以后迁移底层的数据库框架,另一方面是因为 CoreData 中的数据库模型类型,如果使用不当,容易导致很多问题。比如多线程操作就是一个需要特别注意的方面。

其实,CoreData 的学习曲线并没有传说中的那么陡峭,只要我们肯花些时间学习和实践就一定可以掌握。

 

参考内容:
About Core Data model editor - Apple
Core Data - Documentation - Apple
Core Data Programming Guide - Apple
Generating Code - Apple

 

觉得不错?点个赞呗~

本文链接:了解和采用 CoreData 框架 —— Ficow 陪你学 CoreData

转载声明:本站文章如无特别说明,皆为原创。转载请注明:Ficow Shen's Blog

评论区(期待你的留言)