Core Data 是如何在 SQLite 中儲存資料的

語言: CN / TW / HK

highlight: a11y-dark

原文發表在我的部落格 wwww.fatbobman.com

歡迎訂閱我的公共號:【肘子的Swift記事本】

Core Data 是一個具備資料持久化能力的物件圖框架。相同的物件圖在不同的持久化儲存型別中( SQLite 、XML)的資料組織結構差別較大。如果你瀏覽過 Core Data 生成的 SQLite 資料庫檔案,一定會見過其中包含不少奇怪的表和欄位。本文將對這些表和欄位進行介紹,或許可以換個角度幫助你解開部分疑惑,例如: Core Data 為什麼不需要主鍵、NSManagedObjectID 是如何構成的 、儲存衝突的判斷依據是什麼。

如何獲取 Core Data 的 SQLite 資料庫檔案

可以通過以下集中方法獲取到 Core Data 生成的 SQLite 資料庫檔案:

  • 直接獲取檔案的儲存地址

在程式碼中( 通常放置在 Core Data Stack 中,更多有關 Stack 的資訊,請參閱 掌握 Core Data Stack )直接列印持久化儲存的儲存位置,是最直接、高效的獲取手段:

```swift container.loadPersistentStores(completionHandler: { _, error in if let error = error as NSError? { fatalError("Unresolved error (error), (error.userInfo)") } })

if DEBUG

// 如果你有多個儲存,且儲存在不同的目錄,需依次將其打印出來 if let url = container.persistentStoreCoordinator.persistentStores.first?.url { print(url) }

endif

```

image-20220528103822780

在 Finder 中通過快捷鍵( ⇧⌘ G )或選單命令( 前往資料夾 )可以直接到達檔案所在的位置。

image-20220528103959218

  • 啟用除錯引數

如果你在專案中開啟了 Core Data 的除錯資訊輸出,那麼可以直接在除錯資訊的頂部找到資料庫的路徑地址。

swift -com.apple.CoreData.CloudKitDebug 1

更多有關除錯引數的內容,請參閱 Core Data with CloudKit(四)—— 除錯、測試、遷移及其他

  • 通過斷點查詢

在應用執行過程中,通過任意斷點暫停程式的執行,在除錯視窗中輸入如下命令,即可獲得應用在沙盒中的根路徑。

swift po NSHomeDirectory()

  • 第三方工具

一些第三方工具(例如 RocketSim)提供了直接訪問模擬器中 App 目錄的功能。

rocketSim_get_URL

讀者最好能在開啟一個由 Core Data 生成的 SQLite 資料庫檔案的情況下繼續閱讀接下來的內容

基礎的表與欄位

所謂基礎的表與欄位是指,在沒有啟用其他附加功能(持久化歷史跟蹤、Core Data With CloudKit)的情況下,Core Data 為了滿足基本功能而在 SQLite 資料庫中建立的表( 非實體表 )和在實體表中建立的特殊欄位。

實體對應的表

下圖為使用 Xcode Core Data 模板建立的專案的資料庫結構(僅定義了一個實體 Item,且 Item 只有一個屬性 timestamp ),其中實體 Item 在 SQLite 中對應的表是 ZITEM 。

tableAndFieldInCoreData_tableList1

Core Data 按照如下規則將資料模型中的實體轉換成 SQLite 的格式:

  • 實體對應的表名為 Z + 實體名稱(全部大寫),本例中為 ZITEM
  • 實體中屬性對應的欄位為 Z + 屬性名稱(全部大寫),本例中為 ZTIMESTAMP
  • 對於大寫後名稱一致的屬性(屬性在定義時是大小寫敏感的),將為其他重名屬性新增編號。如 Item 有兩個屬性 timestamp 和 timeStamp ,將在表中建立兩個欄位 ZTIMESTAMP 及 ZTIMESTAMP1
  • 為每個實體表新增三個特殊欄位: Z_PK、Z_ENT、Z_OPT(均為 INTEGER 型別)

  • 如實體定義中包含關係,在實體表中為關係建立對應的欄位或建立對應的中間關係表(詳細內容見後文)

Z_ENT 欄位

每個實體表均在 Z_PRIMARYKEY 表(下文詳述)中進行了登記。該欄位與登記記錄的 Z_ENT 一致。可以將其視為表的 ID 。

Z_PK 欄位

從 1 開始遞增的整數,可以將其視為表的主鍵。Z_PK + Z_ENT ( 主鍵 + 表 ID )是 Core Data 在特定 SQLite 資料檔案中查詢具體條目的關鍵。

Z_OPT 欄位

資料記錄版本號。每一次對資料的修改,均會導致該值加一。

Z_PRIMARYKEY 表

Z_PRIMARYKEY 表是實現通過 Z_PK + Z_ENT 定位資料的基礎。它的主要作用有:

  • 對 Core Data 在 SQLite 中建立的表(所有需要通過 Z_PK + Z_ENT 定位記錄的表,不包括 Z_PRIMARYKEY、Z_METADATA、Z_MODELCACHE)進行登記
  • 標註實體之間的關係(僅針對抽象實體)
  • 記錄實體的名稱(資料模型中定義的名稱)
  • 記錄每個登記表當前已使用的最大 Z_PK 值

Z_ENT

表的 ID。實體表會從編號 1 開始,而為其他系統功能建立的表會從編號 16000 開始。下圖展示了實體 Memo 表中的 Z_ENT 與 Memo 在 Z_PRIMARYKEY 表中記錄的 Z_Ent 欄位的對應關係。

tableAndFieldInCoreData_z_ent_1

tableAndFieldInCoreData_z_ent_2

Z_NAME 欄位

實體在資料模型中的名稱(大小寫敏感),用於從 URL 反向查詢對應資料( 具體應用見下文 )。

Z_SUPER 欄位

如果實體為某個實體( Abstract Entity )的子實體,該值對應其父實體的 Z_ENT 。0 表示該實體沒有父實體。下圖展示了當 Item 為抽象實體,ItemSub 為它的子實體時 Z_SUPER 的情況。

tableAndFieldInCoreData_z_super_1

tableAndFieldInCoreData_z_super_2

Z_MAX 欄位

標記了每個登記表最後使用的 Z_PK 值。在建立新的實體資料時,Core Data 將從 Z_PRIMARYKEY 表中找到對應實體最後使用的 Z_PK 值( Z_MAX ),在此值基礎上加一,作為新記錄的 Z_PK 值,並更新該實體對應的 Z_MAX 值。

Z_METADATA 表

Z_METADATA 表中記錄了與當前 SQLite 檔案有關的資訊,包括:版本、識別符號以及其他元資料。

Z_UUID 欄位

當前資料庫檔案的 ID 標識( UUID 型別)。可以通過託管物件協調器獲取該值。在將 NSManagedObjectID 轉換成可儲存的 URL 時,該值表示對應的持久化儲存。

Z_PLIST 欄位

採用 Plist 的格式儲存的有關持久化儲存的元資料( 不包含持久化儲存的 UUID 標識 )。可以通過持久化儲存協調器來讀取或新增資料。如有需要,開發者還可以在其中儲存與資料庫無關的資料( 可以將其視為通過 Core Data 的資料庫檔案儲存程式配置的另類用法 )。

```swift let coordinate = container.persistentStoreCoordinator guard let store = coordinate.persistentStores.first else { fatalError() } var metadata = coordinate.metadata(for: store) // 獲取元資料( Z_PLIST + Z_UUID ) metadata["Author"] = "fat" // 新增新的元資料 store.metadata = metadata

try! container.viewContext.save() // 除了在建立新的持久化儲存時新增 metadata 外,其他情況下新增的資料都需要顯式呼叫上下文的 save 方法來完成持久化 ```

下圖為將 Z_PLIST 中的資料( BLOB 格式 )匯出成 Plist 格式後的情況:

tableAndFieldInCoreData_z_plist

Z_VERSION 欄位

具體作用未知(估計為 Core Data 的 SQLite 格式版本),當前始終為 1 。

Z_MODELCACHE 表

儘管 Core Data 在 Z_METADATA 表中的 Z_PLIST 中保留了當前使用的資料模型版本的簽名信息,但由於 Z_PLIST 的內容是可更改的,因此為了確保應用正在使用的資料模型版本與 SQLite 檔案中的完全一致,Core Data 在 Z_MODELCACHE 表中儲存了一份與當前 SQLite 資料對應的資料模型的快取版本 (某種 mom 或 omo 的變體)。

Z_MODELCACHE 中的快取資料和元資料中的資料模型簽名共同為資料模型的版本驗證和版本遷移提供了保障。

從資料庫結構中得到的收穫

在對 SQLite 的表和欄位有了一定的瞭解後,一些困擾 Core Data 開發者的問題或許就會得到有效的解釋。

為什麼不需要主鍵

Core Data 通過實體表對應的 Z_MAX 自動為每條新增記錄添加了自增主鍵資料。因此在 Core Data 定義資料模型時,開發者無須為實體特別定義主鍵屬性(事實上也無法建立自增主鍵)。

NSManagedObjectID 的構成

託管物件的 NSManagedObjectID 由:資料庫 ID + 表 ID + 實體表中的主鍵共同構成。在 SQLite 中對應的欄位為 Z_UUID + Z_ENT + Z_PK 。通過將 NSManagedObjectID 轉換成可儲存格式的 URL ,可以將它的構成清晰地展示出來。

swift let url = itemSub.objectID.uriRepresentation()

tableAndFieldInCoreData_nsmanagedObjectID_url

【 檔案(持久化儲存)+ 表 + 行 】的資訊組合也將幫助 Core Data 實現從 URL 轉換為對應的託管物件。

```swift let url = URL(string:"x-coredata://E8B22CEA-8316-45E7-BC08-3FBA516F962C/ItemSub/p1")!

if let objectID = container.persistentStoreCoordinator.managedObjectID(forURIRepresentation: url) { if let itemSub = container.viewContext.object(with: objectID) as? ItemSub { ... } } ```

更多有關從 URL 轉換成託管物件的內容請參閱 在 Spotlight 中展示應用中的 Core Data 資料

如何在資料庫中標識關係

Core Data 利用了在同一個資料庫中僅需依靠 Z_ENT + Z_PK 即可定位記錄的特性來實現了在不同的實體之間標註關係的工作。為了節省空間,Core Data 僅儲存了每個關係記錄的 Z_PK 資料,Z_ENT 則直接由資料模型從 Z_PRIMARYKEY 表中獲取。

在資料庫中建立關係的規則為:

  • 一對多

“一”的一側不建立新的欄位,在“多”的一側為關係建立新的欄位,該欄位對應“一”的 Z_PK 值。欄位名稱為 Z + 關係名稱(大寫)

  • 一對一

關係兩端都新增新的欄位,分別為對應資料的 Z_PK 值

  • 多對一

關係兩端都不新增新的欄位,建立一個表示該多對多關係的新表,並在其中逐行新增關係兩側資料的 Z_PK 值。

下圖中,Item 與 Tag 為多對多關係,Core Data 建立了 Z_2TAGS 表來管理該關係資料。

image-20220528162005978

在啟用了抽象實體的情況下,除了記錄對應關係資料的 Z_PK 值外,還會新增一個欄位以記錄該資料具體屬於哪個 Z_ENT ( 父實體或某個子實體)。

儲存衝突的判斷

Core Data 在儲存資料時,通過樂觀鎖的方式來判斷是否會出現儲存衝突的情況。而樂觀鎖的判斷依據則是根據每條記錄的 Z_OPT 資料,採用了版本號機制。

在資料進行持久化時,如果 Core Data 發現上下文的資料快照中的 Z_OPT 資料與行快取中的不一致,或者行快取中的 Z_OPT 與資料庫檔案不一致,均會認為是發生了儲存衝突。

更多有關儲存衝突的內容,請參閱 關於 Core Data 併發程式設計的幾點提示

用於持久化歷史跟蹤的表

在 CoreData 中,如果你的資料儲存形式是 SQLite(絕大多數的開發者都採用此種方式)且啟用了持久化歷史跟蹤功能,無論資料庫中的資料有了何種變化(刪除、新增、修改等),呼叫此資料庫並註冊了該通知的應用,都會收到一個“資料庫有變化”的系統提醒。

近幾年隨著 App Group、小元件、Core Data with CloudKit 、Core Data in Spotlight 等功能的應用,越來越多的 Core Data 應用中都主動或被動地開啟了持久化歷史跟蹤選項。在啟用了該功能後( desc.setOption(true as NSNumber,forKey: NSPersistentHistoryTrackingKey) ),Core Data 會在 SQLite 中新建三張表來管理和記錄事務,並且會在 Z_PRIMARYKEY 表中登記這三張表的資訊。

更多詳細的有關持久化歷史跟蹤的內容,請參閱 在 CoreData 中使用持久化歷史跟蹤

tableAndFieldInCoreData_persistent_history_tracing_tables

image-20220528172620831

Z_ATRANSACTIONSTRING 表

為了能夠分辨事務( Transaction )的來源,事務的產生者需要為託管物件上下文設定事務作者,Core Data 將所有的事務作者的資訊都彙總在 Z_ATRANSACTIONSTRING 表中。

swift container.viewContext.transactionAuthor = "fatbobman"

如果開發者也為上下文也設定了名稱,那麼 Core Data 也將為該上下文名稱建立一條記錄

swift container.viewContext.name = "viewContext"

tableAndFieldInCoreData_atransactionString

Core Data 還會為一些其他的系統功能建立預設的作者記錄。在處理事務時,應忽略這些系統作者產生的事務。

Z_PK 和 Z_ENT 的含義與上文中一致,後文將不再贅述

Z_ATRANSACTION 表

你可以將持久化歷史跟蹤的事務理解為在 Core Data 中的某一次持久化過程(比如呼叫上下文的 save 方法)。Core Data 將與某次事務有關的資訊儲存在 Z_ATRANSACTION 表中。其中最為關鍵的資訊是事務建立的時間和事務作者。

image-20220528174541292

ZAUTHORTS 欄位

對應 Z_ATRANSACTIONSTRING 表中的事務作者的 Z_PK 。上圖中對應的是 Z_ATRANSACTIONSTRING 中的 Z_PK 為 1 的 fatbobman 。

ZCONTEXTNAMETS 欄位

如果為建立事務的上下文設定了名稱,則該欄位對應上下文名稱在 Z_ATRANSACTIONSTRING 表中的記錄的 Z_PK 。上圖對應的是 viewContext 。

ZTIMESTAMP 欄位

事務的建立時間。

ZQUERYGEN 欄位

如果為託管物件上下文設定了鎖定查詢令牌( NSQueryGenerationToken ),那麼事務記錄中還會將當時的查詢令牌儲存在 ZQUERYGEN 欄位中 ( BLOB 型別 )。

swift try? container.viewContext.setQueryGenerationFrom(.current)

Z_ACHANGE 表

在一次事務中,通常會包含若干個資料操作(建立、更改、刪除)。Core Data 將每個資料操作都保持在 Z_CHANGE 表中,並通過 Z_PK 與特定的事務進行關聯。

tableAndFieldInCoreData_change

ZCHANGETYPE 欄位

資料操作型別:0 新建 1 更新 2 刪除

ZENTITY 欄位

操作對應的實體表的 Z_ENT

ZENTITYPK 欄位

操作對應的資料記錄在實體表中的 Z_PK

ZTRANSACTIONID 欄位

操作對應的事務在 Z_ATRANSACTION 表中的 Z_PK

從 SQLite 角度認識持久化歷史跟蹤

建立事務

在持久化歷史跟蹤中,建立事務的工作是由 Core Data 自動完成的,大概的流程如下:

  • 從 Z_PRIMARYKEY 表中獲取 Z_ATRANSACTION 的 Z_MAX
  • 使用 Z_PK ( Z_MAX + 1 ) + Z_ENT ( 事務表在 Z_PRIMARYKEY 中對應的 Z_ENT ) + 作者 ID + 時間戳 在 Z_ATRANSACTION 中建立新事務記錄,並更新 Z_MAX
  • 獲取 Z_ACHANGE 的 Z_MAX
  • 在 Z_ACHANGE 中逐條建立資料操作記錄

查詢事務

因為資料庫中只儲存了事務建立的時間戳,因此無論採用哪種查詢方式(時間 Date、令牌 NSPersistentHistoryToken、事務 NSPersistentHistoryTransaction )最終都會轉換成比較時間戳的方式。

  • 時間戳晚於上次當前應用的查詢時間
  • 作者不是當前 App 的作者或其他系統功能作者
  • 獲取滿足上述條件的全部 Z_CHANGE 記錄

合併事務

事務中提取的資料操作記錄( Z_ACHANGE )中包含了完整的操作型別、對應的例項資料位置等資訊,按圖索驥從資料庫中提取實體資料( Z_PK + Z_ENT )並將其合併( 轉換成 NSManagedObjectID )到指定的上下文中。

刪除事務

  • 查詢並提取時間戳早於全部作者( 包含當前應用作者,但不包含系統功能作者 )的最後查詢時間的事務
  • 刪除上述事務( Z_ATRANSACTION )及其對應的操作資料( Z_ACHANGE )。

瞭解上述過程對理解 Persistent History Tracking Kit 的程式碼很有幫助

其他

如果你的應用使用了 Core Data with CloudKit ,那麼在瀏覽 SQLite 資料結構時你將獲得進一步的驚喜(😱)。Core Data 將建立更多的表來處理與 CloudKit 的同步事宜。考慮到表的複雜性和篇幅,就不繼續展開了。不過有了上文的基礎,瞭解它們的用途也並非很困難。

下圖為開啟了私有資料庫同步功能後 SQLite 中新增的系統表:

image-20220528201143040

這些表主要記載了:CloudKit 私有域資訊、上次同步時間、上次同步令牌、匯出操作日誌、匯入操作日誌、待匯出資料、Core Data 關係與 CloudKit 關係對照表、本地資料對應的 CKRecordName、本地資料的 CKRecord 完整映象( 共享公共資料庫 )等等資訊。

隨著 Core Data 功能的不斷增加,將來可能會看到更多的系統功能表。

總結

撰寫本文的主要目的是對我近段時間來的零散研究進行彙總,方便日後查詢。因此即便你已經完全掌握了 Core Data 的外部儲存結構,但最好還是儘量不要直接對資料庫進行操作,蘋果可能在任何時刻改變它的底層實現。

希望本文能夠對你有所幫助。

原文發表在我的部落格 wwww.fatbobman.com

歡迎訂閱我的公共號:【肘子的Swift記事本】