Core Data 入門 (1)

Core Data

什麼是 Core Data ? 在 iOS(OSX) 應用程式中,要儲存資料可以使用資料庫或檔案,以及現在要介紹的 Core Data,所以 Core Data 的用途就是儲存資料。Core Data 是在 OSX 10.4 及 iOS 3.0 之後開始使用,它可以將物件序列化後儲存在 XML、binary(二位元檔)或 SQLite 資料庫。
Core Data 是一個儲存資料的框架,它的底層本質上還是使用 SQLite 資料庫,它提供簡單易用的方式讓你儲存資料,而不用撰寫複雜的 SQL 語法。如果你的專案有使用 Core Data,可以在該 App 的 Document 目錄中找到 sqlite 檔案。

關於效能的問題,到底直接使用 SQLite 好,還是使用 Core Data 好,可以參考這篇文章 iOS Data Storage: Core Data vs. SQLite。簡單來說,Core Data 是以空間換取時間,意思是比較佔記憶體空間,但速度比較快;另外一個好處是,使用 Core Data 的程式碼比較簡單易讀,不會有複雜的 SQL 語法,官方的說法是,會減少 50~70% 的程式碼。

Managed Object Model(託管物件模型)

大部份的 Core Data 的功能,依賴你所建立的綱要(schema)去描述應用程式的實體(Entity),包含屬性及關聯等等。Core Data 使用一個被稱為 Managed object model 的綱要(schema),它是一個 NSManagedObjectModel 物件的實體。

簡單的說,一個 Managed object model 會對應到資料儲存(persistent store)的一組紀錄,這裡的 persistent store 相當於資料庫;而 Managed object model 即一組紀錄,相當於資料表(table)。

由於 Core Data 並不把自己當成關聯式資料庫(雖然它的底層是使用 SQLite,但這只是它的儲存格式之一),它把這些傳統關聯式資料庫的概念抽象化,所以這邊在說明時會在抽象的定義中,以關聯式資料庫的概念來類比。

因此: 一個實體描述(entity description)表示一個實體(即table),實體(Entity)有一個類別名稱,用來表示 Entity,並且會有特質(property)來表示屬性(attribute)及實體間的關聯(relationship)。一群 Entity (table)組合起來就是 Model。使用 NSEntityDescription 物件用來操作實體描述。

在 Core Data 中:
會有一個 Model(=資料庫),
包含至少一個 Entity(=資料表),
這個 Entity 會有屬性及關聯(=欄位)。

範例 - 使用 Core Data 建立基本的 CRUD 功能

註:本範例使用 XCode 7.0 及 Swift 2.0

開啟 XCode ,建立一個 Single View Application 專案: 專案名稱為 CoreDataDemo,記得勾選 Core Data 選項: 當你勾選 Core Data 時,在 AppDelegate.swift 中會被加入 Core Data 相關的程式碼。你會在其中找到像是 "SingleViewCoreData.sqlite" 這段字串,這就是你的 SQLite 資料庫的名稱。

只要在建立專案時有勾選 Core Data,XCode 就會自動產生一個 *.xcdatamodeld 檔案,選擇它會開啟 model (即database)的管理界面: 目前這個資料庫是空的,讓我們來加第一個實體(Entity,即table)。點選下方的 Add Entity,在左邊欄的會出現新的 Entity 項目,你可以點選它來改名,或在右邊欄中改名。我們將名稱改為 "Product": 接著新增這個 Product entity 的屬性(Attirbutes),點選 Attributes 欄位下方的 "+" (或下方的 Add Attribute 也可以)即可新增。加入 name 及 price 兩個屬性,並且選擇它的 Type: 再來要讓程式碼中可以使用這個 Entity,我們要建立一個 Product 類別,並且繼承 NSManagedObject。XCode 可以幫我們自動產生,在選單列中選擇 Editor > Create NSManagedObject Subclass... 選擇要產生的 Data Model 及 Entity 之後,將要產生的檔案選擇建立在專案中。

XCode 會產生兩個檔案:
Product.swift:
import Foundation
import CoreData
class Product: NSManagedObject {
    // Insert code here to add functionality to your managed object subclass
}
Product+CoreDataProperties.swift:
import Foundation
import CoreData
extension Product {
    @NSManaged var name: String?
    @NSManaged var price: NSNumber?
}
Product+CoreDataProperties.swift 用來存放 Entity 的屬性,Product.swift 就專心在該 Entity 要提供的方法。你會注意到這個 Product 類別是繼承 NSManagedObject。

註:如果不知道為什麼這裡會有兩個檔案,可以研究一下 Swift 的 extension 功能,它和 Objective-C 的 Category 是同樣用途。

NSManagedObject 類別的屬性是 @NSManaged,表示它是受管理的,因此我們不必再去寫 getter/setter 或檢查資料型態是否符合等等的工作。

新增

前置工作都完成後,就可以開始寫程式了,首先,我們要新增一筆資料,打開 ViewController.swift,加入新增資料的程式碼:
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext

override func viewDidLoad() {
    super.viewDidLoad()

    self.addProduct("iPhone 6s 16GB", price: 24500)
}

func addProduct(name:String, price:Int) {
    let product = NSEntityDescription.insertNewObjectForEntityForName("Product", inManagedObjectContext: self.moc) as! Product

    product.name = name
    product.price = price

    do {
        try self.moc.save()
    }catch{
        fatalError("Failure to save context: \(error)")
    }
}
註:記得先 import CoreData,否則可能會出現找不到類別的錯誤。

XCode 在 AppDelegate 中幫我們建立了 managedObjectContext 這個參考到 NSManagedObjectContext 類別的變數,任何對資料表的操作都必須透過它,所以第一步就是先取得它。

addProduct() 方法用來新增產品,要新增資料到資料表,必須透過 NSEntityDescription 類別的 insertNewObjectForEntityForName() 方法,第一個參數是 Entity 的名稱,第二個參數則是 NSManagedObjectContext,這個方法會回傳我們要求的 Entity,接著只要對該 Entity 的屬性給值即可。

動作到此為止,資料表中已經有一筆資料了,可是目前的動作都僅存在於記憶體中,如果這時把程式關閉,這些資料就會消失。因此,最後記得要呼叫 NSManagedObjectContext 的 save() 方法真正的把資料儲存下來。

查詢

來看看剛才新增的產品是否有新增成功。加入顯示全部產品的方法:
func showProducts() {
    let request = NSFetchRequest(entityName: "Product")

    do {
        let results = try moc.executeFetchRequest(request) as! [Product]

        for result in results {
            print("Product Name: \(result.name!), Price: \(result.price!)")
        }
    }catch{
        fatalError("Failed to fetch data: \(error)")
    }
}
然後在原本新增產品的後面加入此方法:
self.addProduct("iPhone 6s 16GB", price: 24500)
self.showProducts()
我執行了 3 次後,結果如下:

刪除

因為每次重新執行程式時,就會新增一次重覆的資料,我們並不想這樣,所以在每次重新執行時,就把資料表清空,把裡面的資料全部刪除。
func cleanUpProducts() {
    let request = NSFetchRequest(entityName: "Product")

    do {
        let results = try moc.executeFetchRequest(request) as! [Product]

        for result in results {
            moc.deleteObject(result)
        }

        do {
            try moc.save()
        }catch{
            fatalError("Failure to save context: \(error)")
        }
    }catch{
        fatalError("Failed to fetch data: \(error)")
    }
}
刪除的動作是,先如同查詢的動作一樣,把資料表中的資料全部取出,然後使用 NSManagedObjectContext 物件的 deleteObject() 方法來一一刪除。

現在 viewDidLoad() 裡是這樣:
self.cleanUpProducts()
self.addProduct("iPhone 6s 16GB", price: 24500)
self.showProducts()

更新

更新的動作,就是先找到該筆資料,修改它的屬性值,然後儲存就完成了。這裡假設要新增三筆資料,但有一筆的價格打錯了,於是把它更新。現在的 viewDidLoad():
self.cleanUpProducts()
self.addProduct("iPhone 6s 16GB", price: 24500)
self.addProduct("iPhone 6s 64GB", price: 28500)
self.addProduct("iPhone 6s 128GB", price: 22500)
self.updateProductPrice()
self.showProducts()
我們要找到 22500 這筆資料,把它改成 32500,新增一個方法:
func updateProductPrice(){
    let request = NSFetchRequest(entityName: "Product")
    request.predicate = NSPredicate(format: "name == %@", "iPhone 6s 128GB")

    do{
        let results = try moc.executeFetchRequest(request) as! [Product]
        if (results.count > 0){
            let product = results[0]
            product.price = 32500

            try self.moc.save()
        }
    } catch {
        fatalError("Failed to update data: \(error)")
    }
}
在原本的查詢中,指定 request 的 predicate 屬性來過濾資料,找到後,只要將新的值指定給它,然後儲存,這樣就完成更新了。 NSPredicate 的設定方法和 SQL 語法很像,詳細內容可以參考這裡

以上是最基本的 Core Data 的 CRUD 操作。

參考資料

本文網址:https://blog.tonycube.com/2015/10/swift-core-data.html
Tony Blog 撰寫,請勿全文複製,轉載時請註明出處及連結,謝謝 😀

10 則留言

  1. 您好,如果是在同一個專案裡的View Controller,是不是可以直接導入Core Data,使用NSFetchResultsControllerDelegate,取得最新的資料庫資料?

    這樣的話要如何使用已儲存的資料? 直接 var 一個變數 =product.price ?

    回覆刪除
  2. 同一個專案裡的檔案都可以用,
    NSFetchResultsControllerDelegate 我查API上的說明,
    它是用在當資料有「新增、刪除、移動或更新」時會觸發取得結果,
    這我不熟,要再查資料。

    要取資料可以參考這篇 http://blog.tonycube.com/2015/10/swift-core-data-2.html
    我寫了兩個例子,一個是取全部的資料 getAll(),一個是以名稱查詢 findByName(),
    用類似的寫法就能取得資料,取回來的是 Product 物件,假設你存在變數 p,
    那就是 p.price

    回覆刪除
    回覆
    1. 多謝您的解說,對取得資料有更進一步的認識!!!

      可是我發現我提問的問題似乎沒有講到重點,我目前是用CoreData儲圖片檔(NSData),而我要實做的東西是,將我新增到CoreData後的圖片能夠放到pickerView的陣列裡頭,

      EX: var imageArray = [NSData],imageArray = [UIImage(data: restaurant.image)],然而卻被Xcode說,沒有image這member,但我明明已宣告在Restaurant.swift這檔案裡頭,不知道我是否忽略掉哪個重要細節?

      刪除
    2. 如果你的 xxx.xcdatamodeld 檔案在執行 App 過後又有新增 Attributes 的話,再次執行時,Xcode 會出錯,解決方法是,必須先把前一版的 App 除除,然後再執行就會正常。

      我猜你的問題可能出在這裡。

      ---
      另外幾個重點:
      1.Attribute 的 Type 必須是 Binary Data
      2.在 Product+CoreDataProperties 檔案加入該 Attribute,一樣注意 Type 是 NSData?
      3.儲存圖片大概像這樣

      product.thumbnail = UIImagePNGRepresentation(UIImage(imageLiteral: "ball.png"))

      記得要先 import UIKit。

      4.取出圖片

      if let img = product.thumbnail {
      imgPic.image = UIImage(data: img)
      }

      imgPic 是 ImageView。

      刪除
    3. Tony你好,不好意思又來煩你了QQ,
      我這個畫面是只有使用UIPickerView而已,圖片已經儲存在Core Data裡(NSData)
      單純的想要將圖片檔案置入旋轉的選單而已~!

      我後來 直接宣告
      var restaurants:Restaurant!
      var imageArray = [NSData]()

      func viewDidLoad(){
      imageArray[restaurants.image!]
      }

      func pickerView(pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusingView view: UIView?) -> UIView {

      let pickerImage = UIImageView()

      if component == 0 {
      _ = imageArray[(Int)(dataArray1[row])]
      }

      return pickerImage
      }

      這樣子是沒有bug的,只是當我轉到這個畫面時,就會直接閃退

      刪除
    4. 看看 log 有顯示什麼錯誤訊息,
      不過,
      imageArray[restaurants.image!]

      _ = imageArray[(Int)(dataArray1[row])]
      這兩行怪怪的

      刪除
    5. 我跟你卡在一樣的地方...
      但是我想要把UITableView的資料,顯示在UIPickerView上可以選取...
      但我沒辦法讀取...

      TableView已經儲存資料了也可以顯示...我的語法如下..

      People 是 core data 的類別...

      imageArray = [Person().name!]

      因為我寫好的PickerView 使用的是 imageArray這個陣列

      所以我想說用帶入的方法 但是出現

      CoreData: error: Failed to call designated initializer on NSManagedObject class 'Porject222.Person'

      求解...大師...

      我也試過用轉型的方法

      let tkt = Person().name
      let lol = tkt! as String
      print("\([lol])")

      lol最後變成String沒錯,但是連Print 都印不出值來...如何從
      class Person : NSManagedObject 裡面取值放進陣列...?

      刪除
    6. 錯誤訊息告訴你「Failed to call designated initializer on NSManagedObject class 'Porject222.Person'」

      呼叫 NSManagedObject 的特定初始化方法失敗,你要呼叫
      init(entity:insertIntoManagedObjectContext:)
      這個方法來建立 Person 物件。

      參考資料:
      http://stackoverflow.com/questions/33301250/resolving-failed-to-call-designated-initializer-on-nsmanagedobject-class

      https://developer.apple.com/library/ios/documentation/Cocoa/Reference/CoreDataFramework/Classes/NSManagedObject_Class/index.html#//apple_ref/doc/uid/TP30001171-SW7

      刪除
    7. 感謝參考資料及分享...問題已解決
      連樓主的問題也可以解決。。關鍵在於初始化 -> 呼叫 -> 轉型(取值)以及“部分語法“需要修改,,感謝Tony 大





      刪除
  3. 謝謝你的文章
    我覺得非常實用!!!!

    回覆刪除

留言小提醒:
1.回覆時間通常在晚上,如果太忙可能要等幾天。
2.請先瀏覽一下其他人的留言,也許有人問過同樣的問題。
3.程式碼請先將它編碼後再貼上。(線上編碼:http://bit.ly/1DL6yog)
4.文字請加上標點符號及斷行,難以閱讀者恕難回覆。
5.感謝您的留言,您的問題也可能幫助到其他有相同問題的人。