photo credit: AlyssssylA Autumn Equinox Vaux’s Swifts via photopin (license)

Realm Swift 3.8.0 (記事執筆時の最新)の時点では、Auto increment ID の仕組みは存在しない。

Auto-incrementing properties: Realm has no mechanism for thread-safe/process-safe auto-incrementing properties commonly used in other databases when generating primary keys. (以下略)

(Realm 公式ドキュメントより引用)

なので、必要なら自分で実装しなければならない。

しかし、出来るだけ Auto increment ID を採用しないことを私はおすすめする。ID に連続性は必須ではないし、もしも作成順にレコード(Realmではオブジェクトと呼ぶ)をソートしたりする必要があるのならば、そのためのにプロパティを別途設けるべきである。 ID には 識別子であること以外の役割を持たせるべきではない。

However, in most situations where a unique auto-generated value is desired, it isn’t necessary to have sequential, contiguous, integer IDs. A unique string primary key is typically sufficient. A common pattern is to set the default property value to NSUUID().UUIDString to generate unique string IDs.

(Realm 公式ドキュメントより引用)

それでも Auto increment ID を採用したいというのであれば、この記事はいくらか役に立つだろう。

単純な Auto increment ID

前提として、Personインスタンスの情報を Realm で保存したいものとする。
Person は次のプロパティを持つ。

  • id (primary key)
  • name

次のPersonクラスでは Auto increment ID の単純な実装がされている。

Person.swift
import RealmSwift

class Person: Object {
    @objc dynamic var id: Int = 0
    @objc dynamic var name: String = ""

    override static func primaryKey() -> String? {
        return "id"
    }

    // ID を increment して返す
    static func newID(realm: Realm) -> Int {
        if let person = realm.objects(Person.self).sorted(byKeyPath: "id").last {
            return person.id + 1
        } else {
            return 1
        }
    }

    // increment された ID を持つ新規 Person インスタンスを返す
    static func create(realm: Realm) -> Person {
        let person: Person = Person()
        person.id = newID(realm: realm)
        return person
    }
}

createメソッドを呼び出すと、auto increment された ID を持つ新規 Person インスタンスが得られる。

let realm: Realm = try! Realm()
let firstPerson: Person = Person.create(realm: realm)
firstPerson.name = "Alice"
try! realm.write {
    realm.add(firstPerson)
}

上記のコードを実行すると、 [“id”: 1, “name”: “Alice”] のオブジェクトが保存される。

オブジェクト削除後に起こる問題: ID再利用

前述のコードで id: 1 の firstPerson を保存するだけなら何ら問題はない。

しかしfirstPersonが削除されると、後の新しいオブジェクトで id が使い回されてしまうという問題が起こる。

try! realm.write {
    realm.delete(firstPerson)
}

let secondPerson: Person = Person.create(realm: realm)
secondPerson.name = "Bob"
try! realm.write {
    realm.add(secondPerson)
}

上記コードでは secondPerson の id は 1 となる。

これでは、主キーが持つべき次の性質に反する。

永続的:レコードが一度作られれば、そこに振られた主キーは中身が変化しようとも変化せず、レコードを削除しても再利用すべきではありません。

(https://qiita.com/jkr_2255/items/5a71ff5f8569c5e0f24d より引用)

secondPersonでは、 id が 2 になっていて欲しい。

ID再利用を防いだ Auto increment ID

削除済みオブジェクトで使われていた ID を再利用することなく、 ID が auto incremant されるようにしたい。

そのために、(ID順での)最後のオブジェクトとして常にダミーオブジェクトがあるようにする。
ダミーオブジェクトをわざわざ削除するようなことはない(はず)なので、新しいオブジェクトには何番の ID を採番すればよいのかが常にはっきりしている。

そして、新しく(ダミーでない)オブジェクトを追加したい場合には、ダミーオブジェクトに情報を付加して更新し、続いて新しいダミーオブジェクトを追加する。

では、Person.createメソッドを書き換えよう。

static func create(realm: Realm, asDummy: Bool = false) -> Person {
    if asDummy {
        let newDummyPerson: Person = Person()
        newDummyPerson.id = Person.newID(realm: realm)
        return newDummyPerson
    } else {
        let lastID: Int = (realm.objects(Person.self).sorted(byKeyPath: "id").last?.id)!
        let dummyPerson: Person = realm.object(ofType: Person.self, forPrimaryKey: lastID)!
        let newDummyPerson: Person = Person.create(realm: realm, asDummy: true)
        try! realm.write {
            realm.add(newDummyPerson)
        }
        return dummyPerson
    }
}

これで、secondPersonは id: 2 として保存される。

let try! realm: Realm = Realm()

// DB作成後、まずダミーオブジェクトを追加
let dummyPerson: Person = Person.create(realm: realm, asDummy: true)
try! realm.write {
    realm.add(dummyPerson)
}
// => 1:(dummy)

// firstPerson を保存
let firstPerson: Person = Person.create(realm: realm)
try! realm.write {
    firstPerson.name = "Alice"
}
// => 1:Alice, 2:(dummy)

// firstPerson を削除
try! realm.write {
    realm.delete(firstPerson)
}
// => 2:(dummy)

// secondPerson を保存
let secondPerson: Person = Person.create(realm: realm)
try! realm.write {
    secondPerson.name = "Bob"
}
// => 2:Bob, 3:(dummy)

コード全体

最後に、コード全体を掲載する。
GitHubに同じコードを公開している。

Person.swift
import RealmSwift

class Person: Object {
    @objc dynamic var id: Int = 0
    @objc dynamic var name: String = ""

    override static func primaryKey() -> String? {
        return "id"
    }

    // ID を increment して返す
    static func newID(realm: Realm) -> Int {
        if let person = realm.objects(Person.self).sorted(byKeyPath: "id").last {
            return person.id + 1
        } else {
            return 1
        }
    }

    // increment された ID を持つ新規 Person オブジェクトを返す
    static func create(realm: Realm, asDummy: Bool = false) -> Person {
        if asDummy {
            let newDummyPerson: Person = Person()
            newDummyPerson.id = Person.newID(realm: realm)
            return newDummyPerson
        } else {
            let lastID: Int = (realm.objects(Person.self).sorted(byKeyPath: "id").last?.id)!
            let dummyPerson: Person = realm.object(ofType: Person.self, forPrimaryKey: lastID)!
            let newDummyPerson: Person = Person.create(realm: realm, asDummy: true)
            try! realm.write {
                realm.add(newDummyPerson)
            }
            return dummyPerson
        }
    }

}
ViewController.swift (iOSで動作確認用)
import RealmSwift

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let realm: Realm = try! Realm()

        // delete all
        try! realm.write {
            realm.deleteAll()
        }

        // dummy person
        let dummyPerson: Person = Person.create(realm: realm, asDummy: true)
        try! realm.write {
            realm.add(dummyPerson)
        }
        self.printPeople(realm: realm)

        // first person
        let firstPerson: Person = Person.create(realm: realm)
        try! realm.write {
            firstPerson.name = "Alice"
        }
        self.printPeople(realm: realm)

        // delete first person
        try! realm.write {
            realm.delete(firstPerson)
        }
        self.printPeople(realm: realm)

        // second person
        let secondPerson: Person = Person.create(realm: realm)
        try! realm.write {
            secondPerson.name = "Bob"
        }
        self.printPeople(realm: realm)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    func printPeople(realm: Realm) {
        let people: Array<Person> = Array(realm.objects(Person.self).sorted(byKeyPath: "id"))
        people.forEach { (person: Person) in
            print(person.id, person.name)
        }
        print("---------------")
    }

}

参考

カテゴリー: Tips

hahnah

はーな Webエンジニア。モバイルアプリも少々。Elmが好き。

0件のコメント

コメントを残す

メールアドレスが公開されることはありません。