iOS開発メモ10 (XcodeでCore Dataを使う2)

前回では、基本的なCore Dataの使い方について記述。今回はつづきとして、ありがちな問題や疑問点に対する解決策などの備忘録を残す。

Core Dataのエンティティ属性にAuto Increment機能がない

"NOT NULL"制約みたいなものはあったが、Auto Incrementはないのである。

Core Data経由で、SQLITE形式のデータを登録すると、裏で自動的にZ_PK、Z_ENT、Z_OPTというカラムが追加される。直接sqliteファイルを見てみると以下のようになっている。

> sqlite3 sample.sqlite
SQLite version 3.19.3 2017-06-27 16:14:08
Enter ".help" for usage hints.
sqlite> .schema
:
CREATE TABLE ZPERSON (
  Z_PK INTEGER PRIMARY KEY,
  Z_ENT INTEGER,
  Z_OPT INTEGER,
  ZAGE INTEGER,
  ZGENDER VARCHAR,
  ZNAME VARCHAR );
sqlite> 

このZ_PKがプライマリキーになる。またこの値がNSManagedObjectIDと一致するらしいが、保存前と後で値がことなるらしい。(テンポラリIDとそうでないか)

NSManagedObjectIDは保存前と後で違う値になる

ただ保存後の値のURLパス末尾の値からpを除去すると、Z_OKの値が取れそう。スタックオーバーフローの投稿にもあって、そこでAuto Increment値を取得する方法が提案されていた。

Can we define auto increment attribute in core data?

Personに実装すると以下のような感じ。この返値に+1すれば目的の値になる。

public class Person: NSManagedObject {
    func getPKNumber() -> Int64   {
        let url = self.objectID.uriRepresentation()
        let urlString = url.absoluteString
        if let pN = urlString.components(separatedBy: "/").last {
            let numberPart = pN.replacingOccurrences(of: "p", with: "")
            if let number = Int64(numberPart) {
                return number
            }
        }
        return 0
    }
}

Personエンティティをダンプすると、以下のようなx-coredata://のパスがIDとして設定されている。

<XXX.Person: 0x608000085c30> (entity: Person; id: 0xd0000000012c0000 <x-coredata://DF01E45F-041A-4FA1-9B76-70DA164A20F9/Person/p75> ; data: <fault>)

また、SQLITEファイルを直接参照すると、確かにp抜きのIDが格納されているので、間違いなさそうである。

> sqlite3 sample.sqlite
SQLite version 3.19.3 2017-06-27 16:48:08
Enter ".help" for usage hints.
sqlite> .tables
ZPERSON       Z_METADATA    Z_MODELCACHE  Z_PRIMARYKEY
sqlite> .headers ON
sqlite> select * from ZPERSON;
Z_PK|Z_ENT|Z_OPT|ZAGE|ZGENDER|ZNAME
73|1|1|20|male|swift 太郎
74|1|1|20|male|swift 太郎
75|1|1|20|male|swift 太郎
sqlite> 

ただし上記の方法だと、間違いなく最後に追加されたPersonレコードを取得しないとダメである。そこまで気を使わないといけないのであれば、最初からAuto Increment用のカラムを追加してプログラムで管理したほうがよさそうである。

例えば、エンティティにidという属性を追加する。そしてINSERT時に既存レコードから最大値を取得して、+1する。(これも上記のスタックオーバーフローの投稿にあった)

let fetchRequest:NSFetchRequest<Person> = Person.fetchRequest()
// 昇順ソート(ID値が大きい順)
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: false)]
let fetchData = try! context.fetch(fetchRequest)

var autoIncrementId = 1
// 最初のレコードの`id`値を取得して+1
if let lastRecordID = fetchData.first?.id {
    autoIncrementId = Int(lastRecordID) + 1
}

というわけで、「直接SQLITEにアクセスさせてくれ!」という微妙なCore Dataっぽい。まぁ所詮アプリ内のローカルDBなので、こんなもんなんでしょうか?大規模なデータ管理システムであれば、外部DBへ通信してJsonやXMLで取得するはずなので、割り切ったほうがよさそうです。

カラムを追加したら、実行エラーになった

前述のときにPersionクラスにidを追加したところ、persistentStoreCoordinatorの呼び出しでエラーになってしまう。これはCore Data内でDBのバージョンを管理しており、既存バージョンのスキーマと異なり、整合性が合わなくなったため発生する。

Core Data Model Versioning and Data Migration

カラムの追加や削除、変更がしたくなった場合の対処方法

まず、Core Dataに対して、新しいバージョンを作成する。

ss--2017-11-04-16.42.50

バージョン名は何でもよい

ss--2017-11-04-16.43.12

進めていくと、データモデルフォルダに新しいモデルが追加される。(既存スキーマが引き継がれる)

ss--2017-11-04-16.44.12

この新しいモデルを選択して、エンティティの属性を変更する。(例では新しい"ID"カラムを追加)

ss--2017-11-04-16.44.28

この新しいモデルで、前回同様にサブクラスを作成する。前回バージョンで作成したサブクラスに対して、属性がアップデートされる。

ss--2017-11-04-16.49.09

モデルを選択して、Model VersionのCurrentを新しいバージョンに変更する。

ss--2017-11-04-17.02.49

先ほどエラーになっていたpersistentStoreCoordinatorの実行時に自動マイグレーションのオプションを渡すように変更する。

let opt = [
  NSInferMappingModelAutomaticallyOption: true,
  NSMigratePersistentStoresAutomaticallyOption: true
]
try coordinator.addPersistentStore(
    ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: opt)

そうすれば、新しいモデルでデータのやり取りが可能になる。カラムを増やした場合、既存データには初期値が追加される。以下の場合だと、Z_PK=75までの既存データには、IDカラムの値は0になっており、マイグレーション後のZ_PK=76のレコードのIDカラムは123というプログラムで設定した値になっている。

> sqlite3 sample.sqlite
SQLite version 3.19.3 2017-06-27 16:48:08
Enter ".help" for usage hints.
sqlite> .tables
ZPERSON       Z_METADATA    Z_MODELCACHE  Z_PRIMARYKEY
sqlite> select * from ZPERSON;
74|1|1|20|0|male|swift 太郎
75|1|1|20|0|male|swift 太郎
76|1|1|20|123|male|swift 太郎
sqlite> 

今回は自動マイグレーション設定で、システム側が自動的に対応してくれたが、大幅な変更があった場合は、シームレスな移行は厳しいような気がする。方法はもちろんあるだろうが、すでにエンドユーザーに配布後の場合は、マイグレーションがうまくいく範囲の変更に留めるような仕様変更を考えないといけないのではないかと思われる。