[第一回] Realmの使い方

iOS

オプショナル型

Realmでは文字列型以外で何故かそのままオプショナル型を使うことができない。

このままではわかりにくいので、以下に例を載せます。

class Coop: Object {
  @objc dynamic var keyword: String = ""   // OK
  @objc dynamic var keyword: String? = nil // OK
  @objc dynamic var id: Int = 0 // OK
  @objc dynamic var id: Int? = nil // NG
}

どうしてこんな奇怪な仕様になっているのかは不明。

解決策

とはいえ、しっかりと解決策があるのでそれを備忘録として残しておこうと思います。

class Coop: Object {
  @objc dynamic var id: Int = 0 // OK
  @objc dynamic var id: Int? = nil // NG
  let id = RealmOptional<Int>() // OK
}

RealmにはRealmOptional型という別のオプショナル型があるのでそれを使います。このときにはDynamicなどの指定は不要です。

ただ、値を代入するときのコードが少し異なるので注意しましょう。

class Coop: Object {
  @objc dynamic var id: Int = 0 // OK
}

let c = Coop()
c.id = 100

 普通の型の場合はこのように代入ができますが、RealmOptional型は以下のようなコードを書かなければいけません。

class Coop: Object {
  let id = RealmOptional<Int>() // OK
}

let c = Coop()
c.id.value = 100

valueに代入しなければいけないのだ。

JSONとの組み合わせ

よくあるのが、JSONにはnullが代入されていてそれを上手いことRealmに代入したいという場合です。

例えば、サーモンランAPIにはジョブの失敗理由であるfailure_reasonというキーがあります。

ところがクリアしていればこの値はないのでnullが代入されています。それを上手く扱おうというわけですね。

class Coop: Object {
  @objc dynamic var failure_reason: String? = nil // OK
  let failure_wave = RealmOptional<Int>() // OK
}

let json = JSON() // JSON DATA
let job = Coop()

job.failure_reason.value = (json["failure_wave"] == JSON.null ? nil : json["failure_wave"].stringValue)
job.failure_wave.value = (json["failure_wave"] == JSON.null ? nil : json["failure_wave"].intValue)

JSONの値がnullかどうかはJSON.nullで判定できるのでこれを使いましょう。

キー

基本的にはPrimary Keyしかないのですが、いろいろな使い方ができますのでご紹介。

Primary Key

データの重複を避けたり、検索を用意にするために必要なプライマリキーの設定方法。

class Coop: Object {
  @objc dynamic var id: Int = 0 // OK
  override static func primaryKey() -> String? {
    return "id"
  }

ユニークインデックスはないの?

ユニークインデックスとはプライマリキーとは違い、いくつかの複数のカラムをユニークなものとして保証する仕組み。

例えるなら、ズボンは何種類あってもいいけれど色とズボンの組み合わせは一種類しか許さないみたいな感じ。

「赤ズボンと青ズボンと緑ズボン」はいいが、「青ズボンが二つ」あってはいけないといったような感じである。

SQLによってはこのユニークインデックスが実装されているのだが、どうもRealmにはないようだ。

Compound Key

それを解決するのがこの複合キーである。

さて、以下のようなRealmオブジェクトがあったとします。

class SalmonRecordsRealm: Object {
    @objc dynamic var id = 0
    dynamic var detail = List<WaveRecords>()
    override static func primaryKey() -> String? {
        return "id"
    }
}

class WaveRecords: Object {
    @objc dynamic var water_level = 0
    @objc dynamic var event_type = 0
    @objc dynamic var score = 0 // Record
}

今回はwater_levelとevent_typeの組み合わせを保存したいわけでが、WaveRecordsオブジェクトがSalmonRecordsRealmのListになっているところがポイントなわけです。

watereventscore
00100OK
01150OK
1050OK
11100OK
1150NG

この場合だと、waterとeventが共に1なレコードが二つあるのでNGなのね。

Listというのは一対多の関係なので、Listの保存件数には上限がありません。

つまり、クエリを実行すると同じWaveRecordsの値がどんどんデータベースに保存されてしまうわけです。これでは困るので、water_levelとevent_typeはユニークな組み合わせとして保存したいわけですね。

watereventscore
00100OK
00100NG
1050OK
1050NG

何も考えずに実装するとこうなります。同じレコードが重複してしまって、データベースが肥大化します。

じゃあ主キーを使えばいいのかというとそうではありません。もし、water_levelかevent_typeを主キーにするとどちらかが一つの値しか保存できなくなるからです。

watereventscore
00100OK
01150NG
1050OK
11100NG

もし、waterを主キーに設定したらwaterの値が同じものをとれなくなるので “組み合わせ” が表現できません。

class SalmonRecordsRealm: Object {
    @objc dynamic var id = 0
    dynamic var detail = List<WaveRecords>()
    override static func primaryKey() -> String? {
        return "id"
    }
}

class WaveRecords: Object {
    @objc dynamic var water_level = 0
    @objc dynamic var event_type = 0
    @objc dynamic var score = 0 // Record
    @objc dynamic var compoundKey = Int() // CompoundKey
}

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

func configure(tide: Int, event: Int) {
    self.water_level = tide
    self.event_type = event
    self.compoundKey = self.water_level * 10 + self.event_type
}

それを解決するコードがこれです。

まず最初に新たにcompoundKeyという名前のカラムを追加します。

別にカラム名は何でもいいですが、ここではcompoundKeyを採用します。

overrideで主キーを先程追加したcompoundKeyにし、関数configure()を設定します。

このconfigure()も別に名前は何でもいいです。大事なのはこのconfigure()の中でユニークな値を生成することです。

さて、先程water_idとevent_typeの組み合わせをユニークにしたいと説明したので、configure()の引数はwaterとeventにします。

self.compoundKey = self.water_level * 10 + self.event_type

ここからこの計算式でcompoundKeyを生成すればwater_levelとevent_typeの値組み合わせが異なれば必ず異なる値が生成されます。つまり、ユニーク性が保証されます。

あとは、以下のような感じでデータベースに値を保存していく過程でcompoundKeyを設定すればいいわけですね。

detail.event_type = 1
detail.water_level = 0
detail.configure(tide: detail.water_level, event: detail.event_type)

try realm.write {
    realm.add(record, update: Realm.UpdatePolicy.all)
}

これってコーディングミスで引数に違う値を入れちゃったりしない?

より安全?

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

func configure() {
    self.compoundKey = self.water_level * 10 + self.event_type
}

こちらの書き方だと引数のコーディングミスをしないので安全かもしれないが、ちゃんと動くかは不明。でも多分動くでしょ、うん。

コメント

タイトルとURLをコピーしました