【SwiftUI】Realmのマイグレーション方法!Migration is required due to the following errorsの解決法
この記事からわかること
- Swift UIでRealm Swiftの
操作 方法 - Migration is required due to the following errorsの解決法
- 既存のデータベース定義を変更する際の注意点
- SchemaVersionとは?
- Configurationとは?
- migrationBlockとは?
- プロパティを追加/削除/プロパティ名の変更/結合した場合のmigration
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
モバイル向けのデータベースを提供しているRealm Swiftの扱いの中でマイグレーションの方法をまとめていきます。
エラー:Migration is required due to the following errors
元々運用していたRealmのテーブル定義を途中でプロパティを追加したり、プライマリーキーを後から追加したいことがあると思います。
class User: Object {
@Persisted var id:String = ""
@Persisted var name:String = ""
+ @Persisted var age:Int = 0 // 後から追加
}
するとアプリ実行させると以下のようなエラーを吐いてアプリが停止してしまいます。
Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=io.realm Code=10 "Migration is required due to the following errors:
これは既に保存されているテーブル情報と新規で操作しようとしているテーブル情報が一致しないために起きてしまいます。これを解決する手段としてRealm Swiftでは「マイグレーション」と呼ばれる機能が用意されています。
ちなみに既存のデータベースデータを削除してしまえばエラーは発生しなくなりますが、ユーザーが溜めたデータをバージョンアップ(テーブル定義の変更)のたびにリセットされたらたまったもんじゃないですもんね。。
マイグレーションとは?
マイグレーションとは日本語で「移行」の意味をもつ英単語です。Realmではデータの整合性を保つためにあらかじめ変更点を定義して設定に組み込んでおくことで整合性を保ってくれる機能のことを指します。
マイグレーションのポイント
- SchemaVersion
- Configuration
上記のポイントは後述するのでまずはコードを見てみます。エラーを発生させないためにはRealm構造体をインスタンス化する前に下記の#1と#2のコードを実行させることです。
ageプロパティを追加した場合
let config = Realm.Configuration(schemaVersion: 1) // #1
Realm.Configuration.defaultConfiguration = config // #2
let relam = try! Realm()
try! relam.write{
var userTable = relam.objects(User.self)
}
SchemaVersion
Realmではmigrationの回数(version)を自分で管理する必要があります。初めてデータ変更した際は0になっているので値に1を指定します。
これは変更するたびに自分でインクリメントしていくので、再度プロパティを追加した場合は先ほどのコードのschemaVersion: 2
に変更する必要があります。変更しないと同様のエラーが発生してしまいます。
// さらに別のプロパティを追加した場合
let config = Realm.Configuration(schemaVersion: 2)
データ型はUInt64
型となっているのでマイナス値(-4)と小数点(1.4)などは指定できません。
Configuration
インクリメントしていくschemaVersionはRealm
構造体のConfiguration
構造体の引数に渡して使用します。
@frozen public struct Configuration {
fileURL: URL? = URL(fileURLWithPath: RLMRealmPathForFile("default.realm"), isDirectory: false),
inMemoryIdentifier: String? = nil,
syncConfiguration: SyncConfiguration? = nil,
encryptionKey: Data? = nil,
readOnly: Bool = false,
★schemaVersion: UInt64 = 0,
★migrationBlock: MigrationBlock? = nil,
deleteRealmIfMigrationNeeded: Bool = false,
shouldCompactOnLaunch: ((Int, Int) -> Bool)? = nil,
objectTypes: [ObjectBase.Type]? = nil,
seedFilePath: URL? = nil) {
}
何やら色々引数に指定できる項目がありますが、重要なのはschemaVersion
とmigrationBlock
です。
schemaVersion
マイグレーションのバージョン番号をインクリメントしながら渡します。
migrationBlock
引数migrationBlock
には古いデータから新しいデータへと移行するために必要な処理を記述します。
プロパティを追加または削除やプライマリーキーを付与したい場合は必要ありませんが、プロパティ名を変更したり値同士を結合したりする場合は明示的にここに変更を記述する必要があります。
MigrationBlock型
public typealias MigrationBlock = (_ migration: Migration, _ oldSchemaVersion: UInt64) -> Void
MigrationBlock
型はタイプエイリアスなのでコードにすると以下のようになります。oldSchemaVersion
にはインクリメント前のバージョン数が格納されるのでここで中の処理を実行させるための分岐処理を行います。
migrationBlock: { migration, oldSchemaVersion in
if(oldSchemaVersion < 1) {
// 移行処理
}
}
マイグレーションのパターン
ここからは以下のテーブルクラス定義で既に運用していると仮定してマイグレーションを実装してみます。
class User: Object {
@Persisted var id:String = ""
@Persisted var name:String = ""
}
プロパティの追加
class User: Object {
@Persisted var id:String = ""
@Persisted var name:String = ""
+ @Persisted var age:Int = 0
}
プロパティを追加する場合はschemaVersion
をインクリメントするだけでOKです。
let config = Realm.Configuration(schemaVersion: 1)
Realm.Configuration.defaultConfiguration = config
let relam = try! Realm()
try! relam.write{
var userTable = relam.objects(User.self)
print(userTable)
}
プロパティの削除
class User: Object {
@Persisted var id:String = ""
@Persisted var name:String = ""
- @Persisted var age:Int = 0
}
プロパティを削除する場合もschemaVersion
をインクリメントするだけでOKです。
let config = Realm.Configuration(schemaVersion: 2)
Realm.Configuration.defaultConfiguration = config
let relam = try! Realm()
try! relam.write{
var userTable = relam.objects(User.self)
print(userTable)
}
プロパティ名を変更
class User: Object {
@Persisted var id:String = ""
@Persisted var userName:String = ""
}
プロパティ名を変更したい場合はschemaVersion
をインクリメントして、migrationBlock
の中で処理を記述していきます。
renameProperty
メソッドを呼び出しonType
にはクラス.className()
を、from
には旧プロパティ名をto
には新しいプロパティ名を渡します。
let config = Realm.Configuration(
schemaVersion: 3,
migrationBlock: { migration, oldSchemaVersion in
if(oldSchemaVersion < 3) {
migration.renameProperty(onType: User.className(), from: "name", to: "userName")
}
}
)
Realm.Configuration.defaultConfiguration = config
let relam = try! Realm()
try! relam.write{
var userTable = relam.objects(User.self)
print(userTable)
}
プロパティの値を連結させる
class User: Object {
@Persisted var id:String = ""
@Persisted var userName:String = ""
+ @Persisted var idUserName:String = "" // 「"id:userName"」形式で構築
}
プロパティの値を連結させた新しいプロパティを追加したい場合はschemaVersion
をインクリメントして、migrationBlock
の中で処理を記述していきます。
enumerateObjects
メソッドを呼び出すと引数から古いバージョンのオブジェクトと新しいバージョンのオブジェクトに参照できるようになります。
let config = Realm.Configuration(
schemaVersion: 4,
migrationBlock: { migration, oldSchemaVersion in
if(oldSchemaVersion < 4) {
migration.enumerateObjects(ofType: User.className()) { oldObject, newObject in
if let id = oldObject?["id"] as? String {
if let name = oldObject?["userName"] as? String {
newObject!["idUserName"] = id + ":" + name
}
}
}
}
}
)
Realm.Configuration.defaultConfiguration = config
let relam = try! Realm()
try! relam.write{
var userTable = relam.objects(User.self)
print(userTable)
}
プライマリーキーを付与する
class User: Object {
@Persisted var id:String = ""
@Persisted var userName:String = ""
@Persisted var idUserName:String = "" // 「"id:userName"」形式で構築
override static func primaryKey() -> String? {
return "id"
}
}
プライマリーキーを付与する場合はschemaVersion
をインクリメントするだけでOKです。
let config = Realm.Configuration(schemaVersion: 6)
Realm.Configuration.defaultConfiguration = config
let relam = try! Realm()
try! relam.write{
var userTable = relam.objects(User.self)
print(userTable)
}
しかしもし既にプライマリーキーに設定したプロパティに重複値が格納されていた場合以下のようなエラーを吐いてアプリが停止してしまいます。
Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=io.realm Code=1 "Primary key property 'class_User.id' has duplicate values after migration." UserInfo={NSLocalizedDescription=Primary key property 'class_User.id' has duplicate values after migration., Error Code=1}
// スレッド 1: 致命的なエラー: 'try!'式は予期せずエラーを発生させました: エラー Domain=io.realm Code=1 "Primary key property 'class_User.id' has duplicate values after migration." UserInfo={NSLocalizedDescription=主キー プロパティ 'class_User.id' は、移行後に重複した値を持っています。エラー コード = 1}
この場合は既に格納済みの重複値を取り除くしか方法はないのでデータを丸々削除するか、重複したレコードを除去する必要があります。
Realmを操作するModelを使用している場合
MVVMアーキテクチャなどに準じたアプリ設計を行なっている場合はRealmのデータベース操作をModelなどにまとめることが多いと思います。
その場合は以下のようにクラスのプロパティにRealmインスタンスを保持させ、CRUD処理をメソッドとして用意すると思います。
class RealmDatabaseModel {
private let realm:Realm! = try! Realm()
public func createUser(record:User){
try! realm.write {
realm.add(record)
}
}
}
上記のようなクラスを定義している場合にマイグレーションするにはプロパティへのRealmインスタンス格納を初期値ではなくイニシャライザを介することで実行することができるようになります。
class RealmDatabaseModel {
// イニシャライザからプロパティの値を格納し、その際にマイグレーションを実行する
init() {
let config = Realm.Configuration(schemaVersion: 1)
realm = try! Realm(configuration:config)
}
// varに変更し、初期値をなくす
private var realm:Realm!
public func createUser(record:User){
try! realm.write {
realm.add(record)
}
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。
私がSwift UI学習に使用した参考書
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。