Swift CoreData Entityにインデックスを追加する


概要

既存のCoreData Entityのカラムに対してindexを作成する方法。


失敗ケース

ios - Why Xcode does not show index options for CoreData entities and attributes? - Stack Overflow

ios - Core data indexing in iOS11 - Stack Overflow

を参考にしながら、以下のようなIssue Entityのtitleカラムにindexを作成するコードを書いていたんだけど...

let attributeTitle = NSAttributeDescription()
attributeTitle.name = #keyPath(Issue.title)
attributeTitle.attributeType = .stringAttributeType
attributeTitle.isOptional = false

let titleIndex = NSFetchIndexElementDescription(property: attributeTitle, collationType: .binary)
titleIndex.isAscending = true
let createTitleIndex = NSFetchIndexDescription(name: "index_issue_title", elements: [titleIndex])

let entity = NSEntityDescription.entity(forEntityName: "Issue", in: (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext)
entity?.indexes.append(createTitleIndex)```

結局、entity?.indexes.append(createTitleIndex)の部分で

erminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Can't modify an immutable model.'

と出て終了。どうやらEntity作成時しかindexesを指定できないんじゃないかな...


CoreDataを使わずに無理矢理作成する

正しい方法じゃないと思うけど、以下の手順でindexを作成した。


CoreDateが発行するSQLをdumpする

まずは、CoreDataが作成したsqliteのdataファイルとTABLE名を確認する為、CoreDataが発行するSQLを確認できるよう準備を行う。

以下のようにEditSchemeにArgumentsに-com.apple.CoreData.SQLDebug 1を指定すればよい。

f:id:letitride:20190825092605p:plainf:id:letitride:20190825092611p:plain

すると次項以降のようなログがdumpされる。

sqlite3のデータファイルを確認

吐き出されたのログの以下の部分で確認できる。多分、Application Support/ の下にproject名で作成されるルールだと思う。

CoreData: annotation: Connecting to sqlite database file at "/Users/ichikawafumiya/Library/Developer/CoreSimulator/Devices/0F566D1B-6BFC-4917-9ADD-3F16AF8E2159/data/Containers/Data/Application/D5181473-B839-40E6-959A-FA6B7E2DCABB/Library/Application Support/TodoWithLocation.sqlite"


作成されたTABLEを確認

IssueというEntityを作成した時は以下のようなDDLが実行される。多分、頭にZが追加されるルールだと思う。

CoreData: sql: CREATE TABLE ZISSUE ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZTITLE VARCHAR ) 


CREATE INDEXを実行する

コード上から実行

import SQLite3
func createIndex(){
    //対象のEntityインスタンスを作成することでdatafileとCoreData Schemaが作成される
    Issue(context: (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext)
    //ここは上記で確認したdataファイル名に変更
    let fileURL = try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("datafile.sqlite")
    var db: OpaquePointer?
    if sqlite3_open(fileURL.path, &db) != SQLITE_OK {
        print("Error: database file open error.")
    }
    //上記で確認したTABLE名と作成対象のカラム名を指定
    sqlite3_exec(db, "create index issue_title_index on ZISSUE ( ZTITLE )", nil, nil, nil)
    sqlite3_close(db)
}

別に複数回発行しても問題なかったんだけど、Userdefaultsなんかに発行記録を設けて、再度の発行を抑止するとよいと思う。

上記、createIndexメソッドを2度発行すると、

[logging] index issue_title_index already exists

と出てindexが作成されたことがわかる。CoreDataから発行したDDLではないので、CREATE INDEXのSQLがログに出るわけではないので注意。


indexアクセスの確認

ターミナルから、上記で確認したdataファイルへアクセス

$ sqlite3 /Users/ichikawafumiya/Library/Developer/CoreSimulator/Devices/0F566D1B-6BFC-4917-9ADD-3F16AF8E2159/data/Containers/Data/Application/268E2223-CC2E-403C-A2D1-8189BCB96E8A/Library/"Application Support"/TodoWithLocation.sqlite

Application Supportは""で括るとよい。

sqlite> explain query plan select * from ZISSUE where ZTITLE = 'text';
QUERY PLAN
`--SEARCH TABLE ZISSUE USING INDEX ISSUE_TITLE_INDEX ZTITLE =?

のような感じでUSING INDEX が確認できる。