概要
既存の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
を指定すればよい。
すると次項以降のようなログが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 が確認できる。