【SwiftUI】Realmのマイグレーション方法!Migration is required due to the following errorsの解決法

この記事からわかること

  • Swift UIRealm 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ではデータの整合性を保つためにあらかじめ変更点を定義して設定に組み込んでおくことで整合性を保ってくれる機能のことを指します。

マイグレーションのポイント

上記のポイントは後述するのでまずはコードを見てみます。エラーを発生させないためには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) {
}

何やら色々引数に指定できる項目がありますが、重要なのはschemaVersionmigrationBlockです。

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学習に使用した参考書

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index